Repository: smallnest/rpcx
Branch: master
Commit: d57f1f75012c
Files: 128
Total size: 428.0 KB
Directory structure:
gitextract_r3mw_qpx/
├── .github/
│ └── workflows/
│ └── go.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── _testutils/
│ ├── GoUnusedProtection__.go
│ ├── arith_service.go
│ ├── arith_service.proto
│ ├── thrift_arith_service-consts.go
│ ├── thrift_arith_service.go
│ └── thrift_arith_service.thrift
├── client/
│ ├── cache_client_builder.go
│ ├── circuit_breaker.go
│ ├── circuit_breaker_test.go
│ ├── client.go
│ ├── client_test.go
│ ├── connection.go
│ ├── connection_iouring.go
│ ├── connection_iouring_test.go
│ ├── connection_kcp.go
│ ├── connection_memu.go
│ ├── connection_nonkcp.go
│ ├── connection_nonquic.go
│ ├── connection_quic.go
│ ├── connection_rdma.go
│ ├── discovery.go
│ ├── dns_discovery.go
│ ├── failmode_enumer.go
│ ├── geo_utils.go
│ ├── hash_utils.go
│ ├── mdns_discovery.go
│ ├── mode.go
│ ├── multiple_servers_discovery.go
│ ├── oneclient.go
│ ├── oneclient_pool.go
│ ├── oneclient_pool_test.go
│ ├── peer2peer_discovery.go
│ ├── ping_utils.go
│ ├── plugin.go
│ ├── selectmode_enumer.go
│ ├── selector.go
│ ├── selector_test.go
│ ├── smooth_weighted_round_robin.go
│ ├── xclient.go
│ ├── xclient_pool.go
│ ├── xclient_pool_test.go
│ └── xclient_test.go
├── clientplugin/
│ └── req_rate_limiting_redis.go
├── codec/
│ ├── codec.go
│ ├── codec_test.go
│ └── testdata/
│ ├── GoUnusedProtection__.go
│ ├── gen.sh
│ ├── protobuf.pb.go
│ ├── protobuf.proto
│ ├── thrift_colorgroup-consts.go
│ ├── thrift_colorgroup.go
│ └── thrift_colorgroup.thrift
├── errors/
│ ├── error.go
│ └── error_test.go
├── go.mod
├── go.sum
├── log/
│ ├── default_logger.go
│ ├── dummy_logger.go
│ └── logger.go
├── protocol/
│ ├── compressor.go
│ ├── compressor_test.go
│ ├── message.go
│ ├── message_chan_test.go
│ ├── message_test.go
│ └── testdata/
│ ├── benchmark.pb.go
│ ├── benchmark.proto
│ └── gen.sh
├── reflection/
│ ├── server_reflection.go
│ └── server_reflection_test.go
├── server/
│ ├── context.go
│ ├── converter.go
│ ├── converter_test.go
│ ├── file_transfer.go
│ ├── gateway.go
│ ├── jsonrpc2.go
│ ├── jsonrpc2_wire.go
│ ├── kcp.go
│ ├── listener.go
│ ├── listener_linux.go
│ ├── listener_rdma.go
│ ├── listener_unix.go
│ ├── memconn.go
│ ├── option.go
│ ├── option_test.go
│ ├── options.go
│ ├── plugin.go
│ ├── plugin_test.go
│ ├── pool.go
│ ├── pool_test.go
│ ├── quic.go
│ ├── router.go
│ ├── server.go
│ ├── server_test.go
│ ├── service.go
│ ├── service_test.go
│ └── stream.go
├── serverplugin/
│ ├── alias.go
│ ├── blacklist.go
│ ├── mdns.go
│ ├── metrics.go
│ ├── rate_limiting.go
│ ├── redis.go
│ ├── registry_test.go
│ ├── req_rate_limiting.go
│ ├── req_rate_limiting_redis.go
│ ├── tee.go
│ └── whitelist.go
├── share/
│ ├── context.go
│ ├── context_test.go
│ ├── share.go
│ └── share_test.go
├── tool/
│ └── xgen/
│ ├── README.md
│ ├── parser/
│ │ └── parser.go
│ └── xgen.go
└── util/
├── buffer_pool.go
├── buffer_pool_test.go
├── compress.go
├── compress_test.go
├── converter.go
├── net.go
└── net_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/go.yml
================================================
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
================================================
FILE: .gitignore
================================================
#
vendor/
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
.idea
cover.html
.history
================================================
FILE: .travis.yml
================================================
language: go
go:
- 1.17.x
env:
- GO111MODULE=on
before_script:
- rm -f go.sum
- go get -tags "quic kcp" -v github.com/smallnest/rpcx@HEAD
- go get github.com/mattn/goveralls
script:
- go test -v ./...
- goveralls -service=travis-ci
notifications:
email:
recipients: smallnest@gmail.com
on_success: change
on_failure: always
================================================
FILE: CHANGELOG.md
================================================
# [rpcx](http://rpcx.io)
## 1.9.0
- unregister all services on close automatically
- add PostHTTPRequestPlugin
- support io_uring
- add CacheDiscovery
- add Oneshot method for XClient
- support RDMA
## 1.8.0
- supports distributed rate limiter based on go-redis/redis-rate
- move zookeeper plugin to https://github.com/smallnest/rpcx-zookeepr
- move consul plugin to https://github.com/smallnest/rpcx-consul
- move redis plugin to https://github.com/smallnest/rpcx-redis
- move influxd/opentelemetry plugin to https://github.com/smallnest/rpcx-plugins
- you can write customized error, for example `{"code": 500, err: "internal error"}`
- server support the work pool by `WithPool`
- support to write services like `go std http router` style without reflect
- simplify async write for service
- improve performance
## 1.7.0
- move etcd support to github.com/rpcxio/rpcx-etcd
- Broken API: NewXXXDiscovery returns error instead of panic
- support AdvertiseAddr in FileTransfer
- support Auth for OneClientPool
- support Auth for XClientPool
- Broken API: add meta parameter for SendFile/DownloadFile
- support streaming between server side and client side
- support DNS as service discovery
- support rpcx flow tracing
- support websocket as the transport like tcp,kcp and quic
- add CMuxPlugin to allow developing customzied services by using the same single port
- re-tag rpcx to make sure the version is less than 2 (for go module)
- support visit grpc services by rpcx clients: https://github.com/rpcxio/rpcxplus/tree/master/grpcx
- support configing grpc servicves in rpcx server side
- improve rpcx performance
- add Inform method in XClient
- add memory connection for unit tests
- supports opentelemetry
## 1.6.0
- support reflection
- add kubernetes config example
- improve nacos support
- improve message.Encode performance
- re-register services in etcd v3
- avoid duplicated client creation
- add SelectNodePlugin that can interrupt the Select method
- support TcpCopy by TeePlugin
- support reuseport for http invoke
- return reply even in case of server errors
- Change two methods' name of client plugin!
- Broken API: add error parameter in `PreWriteResponse`(#486)
- Broken API: change ReadTimeout/WriteTimeout to IdleTimeout
- Support passing Deadline of client contexts to server side
- remove InprocessClient plugin
- use heartbeat/tcp_keepalive to avoid client hanging
## 1.5.0
- support jsonrpc 2.0
- support CORS for jsonrpc 2.0
- support opentracing and opencensus
- upload/download files by streaming
- add Pool for XClient and OneClient
- remove rudp support
- add ConnCreated plugin. Yu can use it to set KCP UDPSession
- update client plugins. All plugin returns error instead of bool
- support ETCD 3.0 API
- support redis as registry
- support redis DB selection
- fix RegisterFunction issues
- add Filter for clients
- remove most of build tags such as etcd, zookeeper,consul,reuseport
- add Nacos as registry http://nacos.io
- support blacklist and whitlist
## 1.4.0
- Support utp and rudp
- Add OneClient to support invoke multile servicesby one client
- Finish compress feature
- Add more plugins for monitoring connection
- Support dynamic port allocation
- Use go module to manage dependencies
- Support shutdown graceful
- Add [rpcx-java](https://github.com/smallnest/rpcx-java) to support develop raw java services and clients
- Support thrift codec
- Setup rpcx offcial site: http://rpcx.io
- Add Chinese document: http://cn.doc.rpcx.io or https://smallnest.gitbooks.io/go-rpc-programming-guide
## 1.3.1
- Add http gateway: https://github.com/rpcxio/rpcx-gateway
- Add direct http invoke
- Add bidirectional communication
- Add xgen tool to generate codes for services automatically
fix bugs.
## 1.3.0
- Rewrite rpcx. It implements its protocol and won't implemented based on wrapper of go standard rpc lib
- Add go tags for pluggable plugins
- Add English document: https://github.com/smallnest/rpcx-programming
- Add rpcx 3.0 examples: https://github.com/rpcxio/rpcx-examples
rpcx 3.0 is not compatible with rpcx 2.0 and below
================================================
FILE: LICENSE
================================================
Copyright 2017 Chao yuepan
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.
================================================
FILE: Makefile
================================================
WORKDIR=`pwd`
default: build
vet:
go vet ./...
tools:
go get github.com/golangci/golangci-lint/cmd/golangci-lint
go get github.com/golang/lint/golint
go get github.com/axw/gocov/gocov
go get github.com/matm/gocov-html
golangci-lint:
golangci-lint run -D errcheck --build-tags 'quic kcp'
lint:
golint ./...
doc:
godoc -http=:6060
deps:
go list -f '{{ join .Deps "\n"}}' ./... |grep "/" | grep -v "github.com/smallnest/rpcx"| grep "\." | sort |uniq
fmt:
go fmt ./...
build:
go build ./...
build-all:
go build -tags "kcp quic" ./...
test:
go test -race -tags "kcp quic" ./...
cover:
gocov test -tags "kcp quic" ./... | gocov-html > cover.html
open cover.html
check-libs:
GIT_TERMINAL_PROMPT=1 GO111MODULE=on go list -m -u all | column -t
update-libs:
GIT_TERMINAL_PROMPT=1 GO111MODULE=on go get -u -v ./...
mod-tidy:
GIT_TERMINAL_PROMPT=1 GO111MODULE=on go mod tidy
================================================
FILE: README.md
================================================
- **stable branch**: v1.7.x
- **development branch**: master
Official site: [http://rpcx.io](http://rpcx.io/)
[](https://opensource.org/licenses/Apache-2.0) [](http://godoc.org/github.com/smallnest/rpcx) [](https://github.com/smallnest/rpcx/actions/workflows/go.yml/badge.svg) [](https://goreportcard.com/report/github.com/smallnest/rpcx) [](https://coveralls.io/github/smallnest/rpcx?branch=master) [](_documents/rpcx_dev_qq3.jpg)
**Notice: etcd**
since rpcx 1.7.6, some plugins have been moved to the independent project:
- `etcd` plugin has been moved to [rpcx-etcd](https://github.com/rpcxio/rpcx-etcd)
- `zookeeper` plugin has been moved to [rpcx-zookeeper](https://github.com/rpcxio/rpcx-zookeeper)
- `consul` plugin has been moved to [rpcx-consul](https://github.com/rpcxio/rpcx-consul)
- `redis` plugin has been moved to [rpcx-redis](https://github.com/rpcxio/rpcx-redis)
- `influxdb` plugin has been moved to [rpcx-plugins](https://github.com/rpcxio/rpcx-plugins)
- `opentelemetry` plugin has been moved to [rpcx-plugins](https://github.com/rpcxio/rpcx-plugins)
## Announce
A tcpdump-like tool added: [rpcxdump](https://github.com/smallnest/rpcxdump)。 You can use it to debug communications between rpcx services and clients.

## Cross-Languages
you can use other programming languages besides Go to access rpcx services.
- **rpcx-gateway**: You can write clients in any programming languages to call rpcx services via [rpcx-gateway](https://github.com/rpcxio/rpcx-gateway)
- **http invoke**: you can use the same http requests to access rpcx gateway
- **Java Services/Clients**: You can use [rpcx-java](https://github.com/smallnest/rpcx-java) to implement/access rpcx services via raw protocol.
- **rust rpcx**: You can write rpcx services in rust by [rpcx-rs](https://github.com/smallnest/rpcx-rs)
> If you can write Go methods, you can also write rpc services. It is so easy to write rpc applications with rpcx.
## Installation
install the basic features:
`go get -v github.com/smallnest/rpcx/...`
If you want to use `quic`、`kcp` registry, use those tags to `go get` 、 `go build` or `go run`. For example, if you want to use all features, you can:
```sh
go get -v -tags "quic kcp" github.com/smallnest/rpcx/...
```
**_tags_**:
- **quic**: support quic transport
- **kcp**: support kcp transport
## Which companies are using rpcx?
## Features
rpcx is a RPC framework like [Alibaba Dubbo](http://dubbo.io/) and [Weibo Motan](https://github.com/weibocom/motan).
**rpcx** is created for targets:
1. **Simple**: easy to learn, easy to develop, easy to integrate and easy to deploy
2. **Performance**: high performance (>= grpc-go)
3. **Cross-platform**: support _raw slice of bytes_, _JSON_, _Protobuf_ and _MessagePack_. Theoretically it can be used with java, php, python, c/c++, node.js, c# and other platforms
4. **Service discovery and service governance**: support zookeeper, etcd and consul.
It contains below features
- Support raw Go functions. There's no need to define proto files.
- Pluggable. Features can be extended such as service discovery, tracing.
- Support TCP, HTTP, [QUIC](https://en.wikipedia.org/wiki/QUIC) and [KCP](https://github.com/skywind3000/kcp)
- Support multiple codecs such as JSON, Protobuf, [MessagePack](https://msgpack.org/index.html) and raw bytes.
- Service discovery. Support peer2peer, configured peers, [zookeeper](https://zookeeper.apache.org), [etcd](https://github.com/coreos/etcd), [consul](https://www.consul.io) and [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS).
- Fault tolerance:Failover, Failfast, Failtry.
- Load banlancing:support Random, RoundRobin, Consistent hashing, Weighted, network quality and Geography.
- Support Compression.
- Support passing metadata.
- Support Authorization.
- Support heartbeat and one-way request.
- Other features: metrics, log, timeout, alias, circuit breaker.
- Support bidirectional communication.
- Support access via HTTP so you can write clients in any programming languages.
- Support API gateway.
- Support backup request, forking and broadcast.
rpcx uses a binary protocol and platform-independent, which means you can develop services in other languages such as Java, python, nodejs, and you can use other prorgramming languages to invoke services developed in Go.
There is a UI manager: [rpcx-ui](https://github.com/smallnest/rpcx-ui).
## Performance
Test results show rpcx has better performance than other rpc framework except standard rpc lib.
The benchmark code is at [rpcx-benchmark](https://github.com/rpcx-ecosystem/rpcx-benchmark).
**Listen to others, but test by yourself**.
**_Test Environment_**
- **CPU**: Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz, 32 cores
- **Memory**: 32G
- **Go**: 1.9.0
- **OS**: CentOS 7 / 3.10.0-229.el7.x86_64
**_Use_**
- protobuf
- the client and the server on the same server
- 581 bytes payload
- 500/2000/5000 concurrent clients
- mock processing time: 0ms, 10ms and 30ms
**_Test Result_**
### mock 0ms process time
| Throughputs | Mean Latency | P99 Latency |
|---|
 |  |  |
### mock 10ms process time
| Throughputs | Mean Latency | P99 Latency |
|---|
 |  |  |
### mock 30ms process time
| Throughputs | Mean Latency | P99 Latency |
|---|
 |  |  |
## Examples
You can find all examples at [rpcxio/rpcx-examples](https://github.com/rpcxio/rpcx-examples).
The below is a simple example.
**Server**
```go
// define example.Arith
……
s := server.NewServer()
s.RegisterName("Arith", new(example.Arith), "")
s.Serve("tcp", addr)
```
**Client**
```go
// prepare requests
……
d, err := client.NewPeer2PeerDiscovery("tcp@"+addr, "")
xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption)
defer xclient.Close()
err = xclient.Call(context.Background(), "Mul", args, reply, nil)
```
## Contributors
## Contribute
see [contributors](https://github.com/smallnest/rpcx/graphs/contributors).
Welcome to contribute:
- submit issues or requirements
- send PRs
- write projects to use rpcx
- write tutorials or articles to introduce rpcx
## License
Apache License, Version 2.0
================================================
FILE: _testutils/GoUnusedProtection__.go
================================================
// Code generated by Thrift Compiler (0.14.0). DO NOT EDIT.
package testutils
var GoUnusedProtection__ int
================================================
FILE: _testutils/arith_service.go
================================================
// Code generated by protoc-gen-gogo.
// source: arith_service.proto
// DO NOT EDIT!
/*
Package client is a generated protocol buffer package.
It is generated from these files:
arith_service.proto
It has these top-level messages:
ProtoArgs
ProtoReply
*/
package testutils
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type ProtoArgs struct {
A int32 `protobuf:"varint,1,opt,name=A,proto3" json:"A,omitempty"`
B int32 `protobuf:"varint,2,opt,name=B,proto3" json:"B,omitempty"`
}
func (m *ProtoArgs) Reset() { *m = ProtoArgs{} }
func (m *ProtoArgs) String() string { return proto.CompactTextString(m) }
func (*ProtoArgs) ProtoMessage() {}
func (*ProtoArgs) Descriptor() ([]byte, []int) { return fileDescriptorArithService, []int{0} }
func (m *ProtoArgs) GetA() int32 {
if m != nil {
return m.A
}
return 0
}
func (m *ProtoArgs) GetB() int32 {
if m != nil {
return m.B
}
return 0
}
type ProtoReply struct {
C int32 `protobuf:"varint,1,opt,name=C,proto3" json:"C,omitempty"`
}
func (m *ProtoReply) Reset() { *m = ProtoReply{} }
func (m *ProtoReply) String() string { return proto.CompactTextString(m) }
func (*ProtoReply) ProtoMessage() {}
func (*ProtoReply) Descriptor() ([]byte, []int) { return fileDescriptorArithService, []int{1} }
func (m *ProtoReply) GetC() int32 {
if m != nil {
return m.C
}
return 0
}
func init() {
proto.RegisterType((*ProtoArgs)(nil), "client.ProtoArgs")
proto.RegisterType((*ProtoReply)(nil), "client.ProtoReply")
}
func (m *ProtoArgs) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ProtoArgs) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.A != 0 {
dAtA[i] = 0x8
i++
i = encodeVarintArithService(dAtA, i, uint64(m.A))
}
if m.B != 0 {
dAtA[i] = 0x10
i++
i = encodeVarintArithService(dAtA, i, uint64(m.B))
}
return i, nil
}
func (m *ProtoReply) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ProtoReply) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.C != 0 {
dAtA[i] = 0x8
i++
i = encodeVarintArithService(dAtA, i, uint64(m.C))
}
return i, nil
}
func encodeFixed64ArithService(dAtA []byte, offset int, v uint64) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
dAtA[offset+4] = uint8(v >> 32)
dAtA[offset+5] = uint8(v >> 40)
dAtA[offset+6] = uint8(v >> 48)
dAtA[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32ArithService(dAtA []byte, offset int, v uint32) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintArithService(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *ProtoArgs) Size() (n int) {
var l int
_ = l
if m.A != 0 {
n += 1 + sovArithService(uint64(m.A))
}
if m.B != 0 {
n += 1 + sovArithService(uint64(m.B))
}
return n
}
func (m *ProtoReply) Size() (n int) {
var l int
_ = l
if m.C != 0 {
n += 1 + sovArithService(uint64(m.C))
}
return n
}
func sovArithService(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozArithService(x uint64) (n int) {
return sovArithService(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *ProtoArgs) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowArithService
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ProtoArgs: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ProtoArgs: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field A", wireType)
}
m.A = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowArithService
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.A |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field B", wireType)
}
m.B = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowArithService
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.B |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipArithService(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthArithService
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *ProtoReply) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowArithService
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ProtoReply: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ProtoReply: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field C", wireType)
}
m.C = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowArithService
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.C |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipArithService(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthArithService
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipArithService(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowArithService
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowArithService
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowArithService
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthArithService
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowArithService
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipArithService(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthArithService = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowArithService = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("arith_service.proto", fileDescriptorArithService) }
var fileDescriptorArithService = []byte{
// 126 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4e, 0x2c, 0xca, 0x2c,
0xc9, 0x88, 0x2f, 0x4e, 0x2d, 0x2a, 0xcb, 0x4c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
0x62, 0x4b, 0xce, 0xc9, 0x4c, 0xcd, 0x2b, 0x51, 0x52, 0xe7, 0xe2, 0x0c, 0x00, 0x09, 0x38, 0x16,
0xa5, 0x17, 0x0b, 0xf1, 0x70, 0x31, 0x3a, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xb0, 0x06, 0x31, 0x3a,
0x82, 0x78, 0x4e, 0x12, 0x4c, 0x10, 0x9e, 0x93, 0x92, 0x14, 0x17, 0x17, 0x58, 0x61, 0x50, 0x6a,
0x41, 0x4e, 0x25, 0x48, 0xce, 0x19, 0xa6, 0xd2, 0xd9, 0x49, 0xe0, 0xc4, 0x23, 0x39, 0xc6, 0x0b,
0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0x21, 0x89, 0x0d, 0x6c, 0x8b,
0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xfa, 0x7e, 0x25, 0x8d, 0x7c, 0x00, 0x00, 0x00,
}
================================================
FILE: _testutils/arith_service.proto
================================================
// protoc --gogofaster_out=. arith_service.proto
// mv arith_service.pb.go arith_service_test.go
syntax = "proto3";
package client;
message ProtoArgs {
int32 A = 1;
int32 B = 2;
}
message ProtoReply {
int32 C = 1;
}
================================================
FILE: _testutils/thrift_arith_service-consts.go
================================================
// Code generated by Thrift Compiler (0.14.0). DO NOT EDIT.
package testutils
import (
"bytes"
"context"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"time"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = context.Background
var _ = time.Now
var _ = bytes.Equal
func init() {
}
================================================
FILE: _testutils/thrift_arith_service.go
================================================
// Code generated by Thrift Compiler (0.14.0). DO NOT EDIT.
package testutils
import (
"bytes"
"context"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"time"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = context.Background
var _ = time.Now
var _ = bytes.Equal
// Attributes:
// - A
// - B
type ThriftArgs_ struct {
A int32 `thrift:"a,1" db:"a" json:"a"`
B int32 `thrift:"b,2" db:"b" json:"b"`
}
func NewThriftArgs_() *ThriftArgs_ {
return &ThriftArgs_{}
}
func (p *ThriftArgs_) GetA() int32 {
return p.A
}
func (p *ThriftArgs_) GetB() int32 {
return p.B
}
func (p *ThriftArgs_) Read(ctx context.Context, iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx)
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if fieldTypeId == thrift.I32 {
if err := p.ReadField1(ctx, iprot); err != nil {
return err
}
} else {
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
case 2:
if fieldTypeId == thrift.I32 {
if err := p.ReadField2(ctx, iprot); err != nil {
return err
}
} else {
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
default:
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(ctx); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *ThriftArgs_) ReadField1(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadI32(ctx); err != nil {
return thrift.PrependError("error reading field 1: ", err)
} else {
p.A = v
}
return nil
}
func (p *ThriftArgs_) ReadField2(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadI32(ctx); err != nil {
return thrift.PrependError("error reading field 2: ", err)
} else {
p.B = v
}
return nil
}
func (p *ThriftArgs_) Write(ctx context.Context, oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin(ctx, "ThriftArgs"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if p != nil {
if err := p.writeField1(ctx, oprot); err != nil {
return err
}
if err := p.writeField2(ctx, oprot); err != nil {
return err
}
}
if err := oprot.WriteFieldStop(ctx); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(ctx); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *ThriftArgs_) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin(ctx, "a", thrift.I32, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:a: ", p), err)
}
if err := oprot.WriteI32(ctx, int32(p.A)); err != nil {
return thrift.PrependError(fmt.Sprintf("%T.a (1) field write error: ", p), err)
}
if err := oprot.WriteFieldEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:a: ", p), err)
}
return err
}
func (p *ThriftArgs_) writeField2(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin(ctx, "b", thrift.I32, 2); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:b: ", p), err)
}
if err := oprot.WriteI32(ctx, int32(p.B)); err != nil {
return thrift.PrependError(fmt.Sprintf("%T.b (2) field write error: ", p), err)
}
if err := oprot.WriteFieldEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 2:b: ", p), err)
}
return err
}
func (p *ThriftArgs_) Equals(other *ThriftArgs_) bool {
if p == other {
return true
} else if p == nil || other == nil {
return false
}
if p.A != other.A {
return false
}
if p.B != other.B {
return false
}
return true
}
func (p *ThriftArgs_) String() string {
if p == nil {
return ""
}
return fmt.Sprintf("ThriftArgs_(%+v)", *p)
}
// Attributes:
// - C
type ThriftReply struct {
C int32 `thrift:"c,1" db:"c" json:"c"`
}
func NewThriftReply() *ThriftReply {
return &ThriftReply{}
}
func (p *ThriftReply) GetC() int32 {
return p.C
}
func (p *ThriftReply) Read(ctx context.Context, iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx)
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if fieldTypeId == thrift.I32 {
if err := p.ReadField1(ctx, iprot); err != nil {
return err
}
} else {
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
default:
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(ctx); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *ThriftReply) ReadField1(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadI32(ctx); err != nil {
return thrift.PrependError("error reading field 1: ", err)
} else {
p.C = v
}
return nil
}
func (p *ThriftReply) Write(ctx context.Context, oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin(ctx, "ThriftReply"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if p != nil {
if err := p.writeField1(ctx, oprot); err != nil {
return err
}
}
if err := oprot.WriteFieldStop(ctx); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(ctx); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *ThriftReply) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin(ctx, "c", thrift.I32, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:c: ", p), err)
}
if err := oprot.WriteI32(ctx, int32(p.C)); err != nil {
return thrift.PrependError(fmt.Sprintf("%T.c (1) field write error: ", p), err)
}
if err := oprot.WriteFieldEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:c: ", p), err)
}
return err
}
func (p *ThriftReply) Equals(other *ThriftReply) bool {
if p == other {
return true
} else if p == nil || other == nil {
return false
}
if p.C != other.C {
return false
}
return true
}
func (p *ThriftReply) String() string {
if p == nil {
return ""
}
return fmt.Sprintf("ThriftReply(%+v)", *p)
}
================================================
FILE: _testutils/thrift_arith_service.thrift
================================================
struct ThriftArgs
{
1: i32 a,
2: i32 b,
}
struct ThriftReply
{
1: i32 c,
}
================================================
FILE: client/cache_client_builder.go
================================================
package client
import "sync"
// CacheClientBuilder defines builder interface to generate RPCCient.
type CacheClientBuilder interface {
SetCachedClient(client RPCClient, k, servicePath, serviceMethod string)
FindCachedClient(k, servicePath, serviceMethod string) RPCClient
DeleteCachedClient(client RPCClient, k, servicePath, serviceMethod string)
GenerateClient(k, servicePath, serviceMethod string) (client RPCClient, err error)
}
// RegisterCacheClientBuilder(network string, builder CacheClientBuilder)
var cacheClientBuildersMutex sync.RWMutex
var cacheClientBuilders = make(map[string]CacheClientBuilder)
func RegisterCacheClientBuilder(network string, builder CacheClientBuilder) {
cacheClientBuildersMutex.Lock()
defer cacheClientBuildersMutex.Unlock()
cacheClientBuilders[network] = builder
}
func getCacheClientBuilder(network string) (CacheClientBuilder, bool) {
cacheClientBuildersMutex.RLock()
defer cacheClientBuildersMutex.RUnlock()
builder, ok := cacheClientBuilders[network]
return builder, ok
}
================================================
FILE: client/circuit_breaker.go
================================================
package client
import (
"errors"
"sync/atomic"
"time"
)
var (
ErrBreakerOpen = errors.New("breaker open")
ErrBreakerTimeout = errors.New("breaker time out")
)
// ConsecCircuitBreaker is window sliding CircuitBreaker with failure threshold.
type ConsecCircuitBreaker struct {
lastFailureTime int64
failures uint64
failureThreshold uint64
window time.Duration
}
// NewConsecCircuitBreaker returns a new ConsecCircuitBreaker.
func NewConsecCircuitBreaker(failureThreshold uint64, window time.Duration) *ConsecCircuitBreaker {
return &ConsecCircuitBreaker{
failureThreshold: failureThreshold,
window: window,
}
}
// Call Circuit function
func (cb *ConsecCircuitBreaker) Call(fn func() error, d time.Duration) error {
var err error
if !cb.ready() {
return ErrBreakerOpen
}
if d == 0 {
err = fn()
} else {
c := make(chan error, 1)
go func() {
c <- fn()
close(c)
}()
t := time.NewTimer(d)
select {
case e := <-c:
err = e
case <-t.C:
err = ErrBreakerTimeout
}
t.Stop()
}
if err == nil {
cb.success()
} else {
cb.fail()
}
return err
}
func (cb *ConsecCircuitBreaker) ready() bool {
lastFailureTime := time.Unix(0, atomic.LoadInt64(&cb.lastFailureTime))
if time.Since(lastFailureTime) > cb.window {
cb.reset()
return true
}
failures := atomic.LoadUint64(&cb.failures)
return failures < cb.failureThreshold
}
func (cb *ConsecCircuitBreaker) success() {
cb.reset()
}
func (cb *ConsecCircuitBreaker) fail() {
atomic.AddUint64(&cb.failures, 1)
atomic.StoreInt64(&cb.lastFailureTime, time.Now().UnixNano())
}
func (cb *ConsecCircuitBreaker) Success() {
cb.success()
}
func (cb *ConsecCircuitBreaker) Fail() {
cb.fail()
}
func (cb *ConsecCircuitBreaker) Ready() bool {
return cb.ready()
}
func (cb *ConsecCircuitBreaker) reset() {
atomic.StoreUint64(&cb.failures, 0)
atomic.StoreInt64(&cb.lastFailureTime, time.Now().UnixNano())
}
================================================
FILE: client/circuit_breaker_test.go
================================================
package client
import (
"errors"
"math/rand"
"testing"
"time"
)
func TestConsecCircuitBreaker(t *testing.T) {
count := -1
fn := func() error {
count++
if count >= 5 && count < 10 {
return nil
}
return errors.New("test error")
}
cb := NewConsecCircuitBreaker(5, 100*time.Millisecond)
for i := 0; i < 25; i++ {
err := cb.Call(fn, 200*time.Millisecond)
switch {
case i < 5:
if err.Error() != "test error" {
t.Fatalf("expect %v, got %v", "test error", err)
}
case i >= 5 && i < 10:
if err != ErrBreakerOpen {
t.Fatalf("expect %v, got %v", ErrBreakerOpen, err)
}
case i >= 10 && i < 15:
if err != nil {
t.Fatalf("expect success, got %v", err)
}
case i >= 15 && i < 20:
if err.Error() != "test error" {
t.Fatalf("expect %v, got %v", "test error", err)
}
case i >= 20 && i < 25:
if err != ErrBreakerOpen {
t.Fatalf("expect %v, got %v", ErrBreakerOpen, err)
}
}
if i == 9 { // expired
time.Sleep(150 * time.Millisecond)
}
}
}
func TestCircuitBreakerRace(t *testing.T) {
cb := NewConsecCircuitBreaker(2, 50*time.Millisecond)
routines := 100
loop := 100000
fn := func() error {
if rand.Intn(2) == 1 {
return nil
}
return errors.New("test error")
}
for r := 0; r < routines; r++ {
go func() {
for i := 0; i < loop; i++ {
cb.Call(fn, 100*time.Millisecond)
}
}()
}
}
================================================
FILE: client/client.go
================================================
package client
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/url"
"strconv"
"strings"
"sync"
"time"
circuit "github.com/rubyist/circuitbreaker"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
)
const (
XVersion = "X-RPCX-Version"
XMessageType = "X-RPCX-MesssageType"
XHeartbeat = "X-RPCX-Heartbeat"
XOneway = "X-RPCX-Oneway"
XMessageStatusType = "X-RPCX-MessageStatusType"
XSerializeType = "X-RPCX-SerializeType"
XMessageID = "X-RPCX-MessageID"
XServicePath = "X-RPCX-ServicePath"
XServiceMethod = "X-RPCX-ServiceMethod"
XMeta = "X-RPCX-Meta"
XErrorMessage = "X-RPCX-ErrorMessage"
)
// ServiceError is an error from server.
type ServiceError interface {
Error() string
IsServiceError() bool
}
// NewServiceError creates a ServiceError with the error message.
func NewServiceError(s string) ServiceError {
return strErr(s)
}
// ClientErrorFunc is a function to create a customized error.
var ClientErrorFunc func(res *protocol.Message, e string) ServiceError
type strErr string
func (s strErr) Error() string {
return string(s)
}
func (s strErr) IsServiceError() bool {
return true
}
// DefaultOption is a common option configuration for client.
var DefaultOption = Option{
Retries: 3,
RPCPath: share.DefaultRPCPath,
ConnectTimeout: time.Second,
SerializeType: protocol.MsgPack,
CompressType: protocol.None,
BackupLatency: 10 * time.Millisecond,
MaxWaitForHeartbeat: 30 * time.Second,
TCPKeepAlivePeriod: time.Minute,
BidirectionalBlock: false,
TimeToDisallow: time.Minute,
}
// Breaker is a CircuitBreaker interface.
type Breaker interface {
Call(func() error, time.Duration) error
Fail()
Success()
Ready() bool
}
// CircuitBreaker is a default circuit breaker (RateBreaker(0.95, 100)).
var CircuitBreaker Breaker = circuit.NewRateBreaker(0.95, 100)
// ErrShutdown connection is closed.
var (
ErrShutdown = errors.New("connection is shut down")
ErrUnsupportedCodec = errors.New("unsupported codec")
)
const (
// ReaderBuffsize is used for bufio reader.
ReaderBuffsize = 16 * 1024
// WriterBuffsize is used for bufio writer.
WriterBuffsize = 16 * 1024
)
type seqKey struct{}
// RPCClient is interface that defines one client to call one server.
type RPCClient interface {
Connect(network, address string) error
Go(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call
Call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error
SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error)
Close() error
RemoteAddr() string
RegisterServerMessageChan(ch chan<- *protocol.Message)
UnregisterServerMessageChan()
IsClosing() bool
IsShutdown() bool
GetConn() net.Conn
}
// Client represents a RPC client.
type Client struct {
option Option
Conn net.Conn
r *bufio.Reader
// w *bufio.Writer
mutex sync.Mutex // protects following
seq uint64
pending map[uint64]*Call
closing bool // user has called Close
shutdown bool // server has told us to stop
pluginClosed bool // the plugin has been called
Plugins PluginContainer
ServerMessageChan chan<- *protocol.Message
}
// NewClient returns a new Client with the option.
func NewClient(option Option) *Client {
return &Client{
option: option,
}
}
// RemoteAddr returns the remote address.
func (client *Client) RemoteAddr() string {
return client.Conn.RemoteAddr().String()
}
// GetConn returns the underlying conn.
func (client *Client) GetConn() net.Conn {
return client.Conn
}
// Option contains all options for creating clients.
type Option struct {
// Group is used to select the services in the same group. Services set group info in their meta.
// If it is empty, clients will ignore group.
Group string
// Retries retries to send
Retries int
// RetryInterval is the interval between retries
RetryInterval time.Duration
// Time to disallow the bad server not to be selected
TimeToDisallow time.Duration
// TLSConfig for tcp and quic
TLSConfig *tls.Config
// kcp.BlockCrypt
Block interface{}
// RPCPath for http connection
RPCPath string
// ConnectTimeout sets timeout for dialing
ConnectTimeout time.Duration
// IdleTimeout sets max idle time for underlying net.Conns
IdleTimeout time.Duration
// BackupLatency is used for Failbackup mode. rpcx will sends another request if the first response doesn't return in BackupLatency time.
BackupLatency time.Duration
// Breaker is used to config CircuitBreaker
GenBreaker func() Breaker
SerializeType protocol.SerializeType
CompressType protocol.CompressType
// send heartbeat message to service and check responses
Heartbeat bool
// interval for heartbeat
HeartbeatInterval time.Duration
MaxWaitForHeartbeat time.Duration
// TCPKeepAlive, if it is zero we don't set keepalive
TCPKeepAlivePeriod time.Duration
// bidirectional mode, if true serverMessageChan will block to wait message for consume. default false.
BidirectionalBlock bool
// alaways use the selected server until it is bad
Sticky bool
// not call server message handler
NilCallServerMessageHandler func(msg *protocol.Message)
}
// Call represents an active RPC.
type Call struct {
ServicePath string // The name of the service and method to call.
ServiceMethod string // The name of the service and method to call.
Metadata map[string]string // metadata
ResMetadata map[string]string
Args interface{} // The argument to the function (*struct).
Reply interface{} // The reply from the function (*struct).
Error error // After completion, the error status.
Done chan *Call // Strobes when call is complete.
Raw bool // raw message or not
}
func (call *Call) done() {
if call.Done == nil { // Oneshot
return
}
select {
case call.Done <- call:
// ok
default:
log.Debug("rpc: discarding Call reply due to insufficient Done chan capacity")
}
}
// RegisterServerMessageChan registers the channel that receives server requests.
func (client *Client) RegisterServerMessageChan(ch chan<- *protocol.Message) {
client.ServerMessageChan = ch
}
// UnregisterServerMessageChan removes ServerMessageChan.
func (client *Client) UnregisterServerMessageChan() {
client.ServerMessageChan = nil
}
// IsClosing client is closing or not.
func (client *Client) IsClosing() bool {
client.mutex.Lock()
defer client.mutex.Unlock()
return client.closing
}
// IsShutdown client is shutdown or not.
func (client *Client) IsShutdown() bool {
client.mutex.Lock()
defer client.mutex.Unlock()
return client.shutdown
}
// Go invokes the function asynchronously. It returns the Call structure representing
// the invocation. The done channel will signal when the call is complete by returning
// the same Call object. If done is nil, Go will allocate a new channel.
// If non-nil, done must be buffered or Go will deliberately crash.
func (client *Client) Go(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
call := new(Call)
call.ServicePath = servicePath
call.ServiceMethod = serviceMethod
meta := ctx.Value(share.ReqMetaDataKey)
if meta != nil { // copy meta in context to meta in requests
call.Metadata = meta.(map[string]string)
}
if !share.IsShareContext(ctx) {
ctx = share.NewContext(ctx)
}
call.Args = args
call.Reply = reply
// // we allow done is nil
// if done == nil {
// done = make(chan *Call, 10) // buffered.
// } else {
// // If caller passes done != nil, it must arrange that
// // done has enough buffer for the number of simultaneous
// // RPCs that will be using that channel. If the channel
// // is totally unbuffered, it's best not to run at all.
// if cap(done) == 0 {
// log.Panic("rpc: done channel is unbuffered")
// }
// }
call.Done = done
if share.Trace {
log.Debugf("client.Go send request for %s.%s, args: %+v in case of client call", servicePath, serviceMethod, args)
}
go client.send(ctx, call)
return call
}
// Call invokes the named function, waits for it to complete, and returns its error status.
func (client *Client) Call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error {
return client.call(ctx, servicePath, serviceMethod, args, reply)
}
func (client *Client) call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error {
seq := new(uint64)
if sharedCtx, ok := ctx.(*share.Context); ok {
sharedCtx.SetValue(seqKey{}, seq)
} else {
ctx = context.WithValue(ctx, seqKey{}, seq)
}
if share.Trace {
log.Debugf("client.call for %s.%s, args: %+v in case of client call", servicePath, serviceMethod, args)
defer func() {
log.Debugf("client.call done for %s.%s, args: %+v in case of client call", servicePath, serviceMethod, args)
}()
}
Done := client.Go(ctx, servicePath, serviceMethod, args, reply, make(chan *Call, 10)).Done
var err error
select {
case <-ctx.Done(): // cancel by context
client.mutex.Lock()
call := client.pending[*seq]
delete(client.pending, *seq)
client.mutex.Unlock()
if call != nil {
call.Error = ctx.Err()
call.done()
}
return ctx.Err()
case call := <-Done:
err = call.Error
meta := ctx.Value(share.ResMetaDataKey)
if meta != nil && len(call.ResMetadata) > 0 {
resMeta := meta.(map[string]string)
locker, ok := ctx.Value(share.ContextTagsLock).(*sync.Mutex)
if ok {
locker.Lock()
for k, v := range call.ResMetadata {
resMeta[k] = v
}
resMeta[share.ServerAddress] = client.Conn.RemoteAddr().String()
locker.Unlock()
} else {
for k, v := range call.ResMetadata {
resMeta[k] = v
}
resMeta[share.ServerAddress] = client.Conn.RemoteAddr().String()
}
}
}
return err
}
// SendRaw sends raw messages. You don't care args and replies.
func (client *Client) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) {
if sharedCtx, ok := ctx.(*share.Context); ok {
sharedCtx.SetValue(seqKey{}, r.Seq())
} else {
ctx = context.WithValue(ctx, seqKey{}, r.Seq())
}
call := new(Call)
call.Raw = true
call.ServicePath = r.ServicePath
call.ServiceMethod = r.ServiceMethod
meta := ctx.Value(share.ReqMetaDataKey)
rmeta := make(map[string]string)
// copy meta to rmeta
if meta != nil {
for k, v := range meta.(map[string]string) {
rmeta[k] = v
}
}
// copy r.Metadata to rmeta
if r.Metadata != nil {
for k, v := range r.Metadata {
rmeta[k] = v
}
}
if meta != nil { // copy meta in context to meta in requests
call.Metadata = rmeta
}
r.Metadata = rmeta
if !share.IsShareContext(ctx) {
ctx = share.NewContext(ctx)
}
done := make(chan *Call, 10)
call.Done = done
seq := r.Seq()
client.mutex.Lock()
if client.pending == nil {
client.pending = make(map[uint64]*Call)
}
client.pending[seq] = call
client.mutex.Unlock()
data := r.EncodeSlicePointer()
_, err := client.Conn.Write(*data)
protocol.PutData(data)
if err != nil {
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
if call != nil {
call.Error = err
call.done()
}
return nil, nil, err
}
if r.IsOneway() {
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
if call != nil {
call.done()
}
return nil, nil, nil
}
var m map[string]string
var payload []byte
select {
case <-ctx.Done(): // cancel by context
client.mutex.Lock()
call := client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
if call != nil {
call.Error = ctx.Err()
call.done()
}
return nil, nil, ctx.Err()
case call := <-done:
err = call.Error
m = call.ResMetadata
if call.Reply != nil {
payload = call.Reply.([]byte)
}
}
return m, payload, err
}
func convertRes2Raw(res *protocol.Message) (map[string]string, []byte, error) {
m := make(map[string]string)
m[XVersion] = strconv.Itoa(int(res.Version()))
if res.IsHeartbeat() {
m[XHeartbeat] = "true"
}
if res.IsOneway() {
m[XOneway] = "true"
}
if res.MessageStatusType() == protocol.Error {
m[XMessageStatusType] = "Error"
} else {
m[XMessageStatusType] = "Normal"
}
// if res.CompressType() == protocol.Gzip {
// m["Content-Encoding"] = "gzip"
// }
m[XMeta] = urlencode(res.Metadata)
m[XSerializeType] = strconv.Itoa(int(res.SerializeType()))
m[XMessageID] = strconv.FormatUint(res.Seq(), 10)
m[XServicePath] = res.ServicePath
m[XServiceMethod] = res.ServiceMethod
return m, res.Payload, nil
}
func urlencode(data map[string]string) string {
if len(data) == 0 {
return ""
}
var buf strings.Builder
first := true
for k, v := range data {
if !first {
buf.WriteByte('&')
}
buf.WriteString(url.QueryEscape(k))
buf.WriteByte('=')
buf.WriteString(url.QueryEscape(v))
first = false
}
return buf.String()
}
func (client *Client) send(ctx context.Context, call *Call) {
// Register this call.
client.mutex.Lock()
if client.shutdown || client.closing {
call.Error = ErrShutdown
client.mutex.Unlock()
call.done()
return
}
isHeartbeat := call.ServicePath == "" && call.ServiceMethod == ""
serializeType := client.option.SerializeType
if isHeartbeat {
serializeType = protocol.MsgPack
}
codec := share.Codecs[serializeType]
if codec == nil {
call.Error = ErrUnsupportedCodec
client.mutex.Unlock()
call.done()
return
}
if client.pending == nil {
client.pending = make(map[uint64]*Call)
}
seq := client.seq
client.seq++
client.pending[seq] = call
client.mutex.Unlock()
if cseq, ok := ctx.Value(seqKey{}).(*uint64); ok {
*cseq = seq
}
// req := protocol.NewMessage()
req := protocol.NewMessage()
req.SetMessageType(protocol.Request)
req.SetSeq(seq)
if call.Reply == nil {
req.SetOneway(true)
}
// heartbeat, and use default SerializeType (msgpack)
if isHeartbeat {
req.SetHeartbeat(true)
req.SetSerializeType(protocol.MsgPack)
} else {
req.SetSerializeType(client.option.SerializeType)
}
if call.Metadata != nil {
req.Metadata = call.Metadata
}
req.ServicePath = call.ServicePath
req.ServiceMethod = call.ServiceMethod
data, err := codec.Encode(call.Args)
if err != nil {
client.mutex.Lock()
delete(client.pending, seq)
client.mutex.Unlock()
call.Error = err
call.done()
return
}
if len(data) > 1024 && client.option.CompressType != protocol.None {
req.SetCompressType(client.option.CompressType)
}
req.Payload = data
if client.Plugins != nil {
_ = client.Plugins.DoClientBeforeEncode(req)
}
if share.Trace {
log.Debugf("client.send for %s.%s, args: %+v in case of client call", call.ServicePath, call.ServiceMethod, call.Args)
}
allData := req.EncodeSlicePointer()
_, err = client.Conn.Write(*allData)
protocol.PutData(allData)
if share.Trace {
log.Debugf("client.sent for %s.%s, args: %+v in case of client call", call.ServicePath, call.ServiceMethod, call.Args)
}
if err != nil {
if e, ok := err.(*net.OpError); ok {
if e.Err != nil {
err = fmt.Errorf("net.OpError: %s", e.Err.Error())
} else {
err = errors.New("net.OpError")
}
}
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
if call != nil {
call.Error = err
call.done()
}
return
}
isOneway := req.IsOneway()
if isOneway {
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
if call != nil {
call.done()
}
}
if client.option.IdleTimeout != 0 {
_ = client.Conn.SetDeadline(time.Now().Add(client.option.IdleTimeout))
}
}
func (client *Client) input() {
var err error
for err == nil {
res := protocol.NewMessage()
if client.option.IdleTimeout != 0 {
_ = client.Conn.SetDeadline(time.Now().Add(client.option.IdleTimeout))
}
err = res.Decode(client.r)
if err != nil {
break
}
if client.Plugins != nil {
_ = client.Plugins.DoClientAfterDecode(res)
}
seq := res.Seq()
var call *Call
isServerMessage := (res.MessageType() == protocol.Request && !res.IsHeartbeat() && res.IsOneway())
if !isServerMessage {
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
}
if share.Trace {
log.Debugf("client.input received %v", res)
}
switch {
case call == nil:
if isServerMessage {
if client.ServerMessageChan != nil {
client.handleServerRequest(res)
} else if client.option.NilCallServerMessageHandler != nil {
client.option.NilCallServerMessageHandler(res)
}
continue
}
case res.MessageStatusType() == protocol.Error:
// We've got an error response. Give this to the request
if len(res.Metadata) > 0 {
call.ResMetadata = res.Metadata
// convert server error to a customized error, which implements ServerError interface
if ClientErrorFunc != nil {
call.Error = ClientErrorFunc(res, res.Metadata[protocol.ServiceError])
} else {
call.Error = strErr(res.Metadata[protocol.ServiceError])
}
}
if call.Raw {
call.Metadata, call.Reply, _ = convertRes2Raw(res)
call.Metadata[XErrorMessage] = call.Error.Error()
call.ResMetadata = res.Metadata
} else if len(res.Payload) > 0 {
data := res.Payload
codec := share.Codecs[res.SerializeType()]
if codec != nil {
_ = codec.Decode(data, call.Reply)
}
}
call.done()
default:
if call.Raw {
call.Metadata, call.Reply, _ = convertRes2Raw(res)
call.ResMetadata = res.Metadata
} else {
data := res.Payload
if len(data) > 0 {
codec := share.Codecs[res.SerializeType()]
if codec == nil {
call.Error = strErr(ErrUnsupportedCodec.Error())
} else {
err = codec.Decode(data, call.Reply)
if err != nil {
call.Error = strErr(err.Error())
}
}
}
if len(res.Metadata) > 0 {
call.ResMetadata = res.Metadata
}
}
call.done()
}
}
// Terminate pending calls.
if client.ServerMessageChan != nil {
req := protocol.NewMessage()
req.SetMessageType(protocol.Request)
req.SetMessageStatusType(protocol.Error)
if req.Metadata == nil {
req.Metadata = make(map[string]string)
if err != nil {
req.Metadata[protocol.ServiceError] = err.Error()
}
}
req.Metadata["server"] = client.Conn.RemoteAddr().String()
client.handleServerRequest(req)
}
client.mutex.Lock()
if !client.pluginClosed {
if client.Plugins != nil {
client.Plugins.DoClientConnectionClose(client.Conn)
}
client.pluginClosed = true
}
client.Conn.Close()
client.shutdown = true
closing := client.closing
if e, ok := err.(*net.OpError); ok {
if e.Addr != nil || e.Err != nil {
err = fmt.Errorf("net.OpError: %s", e.Err.Error())
} else {
err = errors.New("net.OpError")
}
}
if err == io.EOF {
if closing {
err = ErrShutdown
} else {
err = io.ErrUnexpectedEOF
}
}
for _, call := range client.pending {
call.Error = err
call.done()
}
client.mutex.Unlock()
if err != nil && !closing {
log.Errorf("rpcx: client protocol error: %v", err)
}
}
func (client *Client) handleServerRequest(msg *protocol.Message) {
defer func() {
if r := recover(); r != nil {
log.Errorf("ServerMessageChan may be closed so client remove it. Please add it again if you want to handle server requests. error is %v", r)
client.ServerMessageChan = nil
}
}()
serverMessageChan := client.ServerMessageChan
if serverMessageChan != nil {
if client.option.BidirectionalBlock {
serverMessageChan <- msg
} else {
select {
case serverMessageChan <- msg:
default:
log.Warnf("ServerMessageChan may be full so the server request %d has been dropped", msg.Seq())
}
}
}
}
func (client *Client) heartbeat() {
t := time.NewTicker(client.option.HeartbeatInterval)
if client.option.MaxWaitForHeartbeat == 0 {
client.option.MaxWaitForHeartbeat = 30 * time.Second
}
for range t.C {
if client.IsShutdown() || client.IsClosing() {
t.Stop()
return
}
request := time.Now().UnixNano()
reply := int64(0)
ctx, cancel := context.WithTimeout(context.Background(), client.option.MaxWaitForHeartbeat)
err := client.Call(ctx, "", "", &request, &reply)
abnormal := false
if ctx.Err() != nil {
log.Warnf("failed to heartbeat to %s, context err: %v", client.Conn.RemoteAddr().String(), ctx.Err())
abnormal = true
}
cancel()
if err != nil {
log.Warnf("failed to heartbeat to %s: %v", client.Conn.RemoteAddr().String(), err)
abnormal = true
}
if reply != request {
log.Warnf("reply %d in heartbeat to %s is different from request %d", reply, client.Conn.RemoteAddr().String(), request)
}
if abnormal {
client.Close()
}
}
}
// Close calls the underlying connection's Close method. If the connection is already
// shutting down, ErrShutdown is returned.
func (client *Client) Close() error {
client.mutex.Lock()
for seq, call := range client.pending {
delete(client.pending, seq)
if call != nil {
call.Error = ErrShutdown
call.done()
}
}
var err error
if !client.pluginClosed {
if client.Plugins != nil {
client.Plugins.DoClientConnectionClose(client.Conn)
}
client.pluginClosed = true
err = client.Conn.Close()
}
if client.closing || client.shutdown {
client.mutex.Unlock()
return ErrShutdown
}
client.closing = true
client.mutex.Unlock()
return err
}
================================================
FILE: client/client_test.go
================================================
package client
import (
"context"
"fmt"
"math/rand"
"net"
"sync"
"testing"
"time"
testutils "github.com/smallnest/rpcx/_testutils"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/server"
)
type Args struct {
A int
B int
}
type Reply struct {
C int
}
type Arith int
func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error {
reply.C = args.A * args.B
return nil
}
type PBArith int
func (t *PBArith) Mul(ctx context.Context, args *testutils.ProtoArgs, reply *testutils.ProtoReply) error {
reply.C = args.A * args.B
return nil
}
func (t *Arith) ThriftMul(ctx context.Context, args *testutils.ThriftArgs_, reply *testutils.ThriftReply) error {
reply.C = args.A * args.B
return nil
}
type Bidirectional struct {
*server.Server
}
func (t *Bidirectional) Mul(ctx context.Context, args *Args, reply *Reply) error {
conn := ctx.Value(server.RemoteConnContextKey).(net.Conn)
reply.C = args.A * args.B
t.SendMessage(conn, "test_service_path", "test_service_method", nil, []byte("abcde"))
return nil
}
func TestClient_IT(t *testing.T) {
s := server.NewServer()
_ = s.RegisterName("Arith", new(Arith), "")
_ = s.RegisterName("PBArith", new(PBArith), "")
go func() {
_ = s.Serve("tcp", "127.0.0.1:0")
}()
defer s.Close()
time.Sleep(500 * time.Millisecond)
addr := s.Address().String()
client := &Client{
option: DefaultOption,
}
err := client.Connect("tcp", addr)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
defer client.Close()
args := &Args{
A: 10,
B: 20,
}
reply := &Reply{}
err = client.Call(context.Background(), "Arith", "Mul", args, reply)
if err != nil {
t.Fatalf("failed to call: %v", err)
}
if reply.C != 200 {
t.Fatalf("expect 200 but got %d", reply.C)
}
err = client.Call(context.Background(), "Arith", "Add", args, reply)
if err == nil {
t.Fatal("expect an error but got nil")
}
client.option.SerializeType = protocol.MsgPack
reply = &Reply{}
err = client.Call(context.Background(), "Arith", "Mul", args, reply)
if err != nil {
t.Fatalf("failed to call: %v", err)
}
if reply.C != 200 {
t.Fatalf("expect 200 but got %d", reply.C)
}
client.option.SerializeType = protocol.ProtoBuffer
pbArgs := &testutils.ProtoArgs{
A: 10,
B: 20,
}
pbReply := &testutils.ProtoReply{}
err = client.Call(context.Background(), "PBArith", "Mul", pbArgs, pbReply)
if err != nil {
t.Fatalf("failed to call: %v", err)
}
if pbReply.C != 200 {
t.Fatalf("expect 200 but got %d", pbReply.C)
}
}
func TestClient_IT_Concurrency(t *testing.T) {
s := server.NewServer()
_ = s.RegisterName("PBArith", new(PBArith), "")
go func() {
_ = s.Serve("tcp", "127.0.0.1:0")
}()
defer s.Close()
time.Sleep(500 * time.Millisecond)
addr := s.Address().String()
client := &Client{
option: DefaultOption,
}
err := client.Connect("tcp", addr)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
defer client.Close()
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
i := i
go testSendRaw(t, client, uint64(i), rand.Int31(), rand.Int31(), &wg)
}
wg.Wait()
}
func testSendRaw(t *testing.T, client *Client, seq uint64, x, y int32, wg *sync.WaitGroup) {
defer wg.Done()
rpcxReq := protocol.NewMessage()
rpcxReq.SetMessageType(protocol.Request)
rpcxReq.SetSeq(seq)
rpcxReq.ServicePath = "PBArith"
rpcxReq.ServiceMethod = "Mul"
rpcxReq.SetSerializeType(protocol.ProtoBuffer)
rpcxReq.SetOneway(false)
pbArgs := &testutils.ProtoArgs{
A: x,
B: y,
}
data, _ := pbArgs.Marshal()
rpcxReq.Payload = data
_, reply, err := client.SendRaw(context.Background(), rpcxReq)
if err != nil {
t.Errorf("failed to call SendRaw: %v", err)
return
}
pbReply := &testutils.ProtoReply{}
err = pbReply.Unmarshal(reply)
if err != nil {
t.Errorf("failed to unmarshal reply: %v", err)
return
}
if pbReply.C != x*y {
t.Errorf("expect %d but got %d", x*y, pbReply.C)
return
}
}
func TestClient_Res_Reset(t *testing.T) {
var res = protocol.NewMessage()
res.Payload = []byte{1, 2, 3, 4, 5, 6, 7, 8}
data := res.Payload
res.Reset()
if len(data) == 0 {
t.Fatalf("data has been set to empty after response has been reset: %v", data)
}
}
func TestClient_Bidirectional(t *testing.T) {
s := server.NewServer()
_ = s.RegisterName("Bidirectional", &Bidirectional{Server: s}, "")
go func() {
_ = s.Serve("tcp", "127.0.0.1:0")
}()
defer s.Close()
time.Sleep(500 * time.Millisecond)
addr := s.Address().String()
opt := DefaultOption
var receive string
opt.NilCallServerMessageHandler = func(msg *protocol.Message) {
fmt.Printf("receive msg from server: %s\n", msg.Payload)
receive = string(msg.Payload)
}
client := &Client{
option: opt,
}
err := client.Connect("tcp", addr)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
defer client.Close()
args := &Args{
A: 10,
B: 20,
}
reply := &Reply{}
err = client.Call(context.Background(), "Bidirectional", "Mul", args, reply)
if err != nil {
t.Fatalf("failed to call: %v", err)
}
if receive != "abcde" {
t.Fatalf("expect abcde but got %s", receive)
}
if reply.C != 200 {
t.Fatalf("expect 200 but got %d", reply.C)
}
}
================================================
FILE: client/connection.go
================================================
package client
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/share"
"golang.org/x/net/websocket"
)
type ConnFactoryFn func(c *Client, network, address string) (net.Conn, error)
var ConnFactories = make(map[string]ConnFactoryFn)
func init() {
ConnFactories["http"] = newDirectHTTPConn
ConnFactories["kcp"] = newDirectKCPConn
ConnFactories["quic"] = newDirectQuicConn
ConnFactories["unix"] = newDirectConn
ConnFactories["memu"] = newMemuConn
ConnFactories["iouring"] = newIOUringConn
}
// Connect connects the server via specified network.
func (client *Client) Connect(network, address string) error {
var conn net.Conn
var err error
switch network {
case "http":
conn, err = newDirectHTTPConn(client, network, address)
case "ws", "wss":
conn, err = newDirectWSConn(client, network, address)
default:
fn := ConnFactories[network]
if fn != nil {
conn, err = fn(client, network, address)
} else {
conn, err = newDirectConn(client, network, address)
}
}
if err == nil && conn != nil {
if tc, ok := conn.(*net.TCPConn); ok && client.option.TCPKeepAlivePeriod > 0 {
_ = tc.SetKeepAlive(true)
_ = tc.SetKeepAlivePeriod(client.option.TCPKeepAlivePeriod)
}
if client.option.IdleTimeout != 0 {
_ = conn.SetDeadline(time.Now().Add(client.option.IdleTimeout))
}
if client.Plugins != nil {
conn, err = client.Plugins.DoConnCreated(conn)
if err != nil {
return err
}
}
client.Conn = conn
client.r = bufio.NewReaderSize(conn, ReaderBuffsize)
// c.w = bufio.NewWriterSize(conn, WriterBuffsize)
// start reading and writing since connected
go client.input()
if client.option.Heartbeat && client.option.HeartbeatInterval > 0 {
go client.heartbeat()
}
}
if err != nil && client.Plugins != nil {
client.Plugins.DoConnCreateFailed(network, address)
}
return err
}
func newDirectConn(c *Client, network, address string) (net.Conn, error) {
var conn net.Conn
var tlsConn *tls.Conn
var err error
if c == nil {
err = fmt.Errorf("nil client")
return nil, err
}
if c.option.TLSConfig != nil {
dialer := &net.Dialer{
Timeout: c.option.ConnectTimeout,
}
tlsConn, err = tls.DialWithDialer(dialer, network, address, c.option.TLSConfig)
// or conn:= tls.Client(netConn, &config)
conn = net.Conn(tlsConn)
} else {
conn, err = net.DialTimeout(network, address, c.option.ConnectTimeout)
}
if err != nil {
log.Warnf("failed to dial server: %v", err)
return nil, err
}
return conn, nil
}
var connected = "200 Connected to rpcx"
func newDirectHTTPConn(c *Client, network, address string) (net.Conn, error) {
if c == nil {
return nil, errors.New("empty client")
}
path := c.option.RPCPath
if path == "" {
path = share.DefaultRPCPath
}
var conn net.Conn
var tlsConn *tls.Conn
var err error
if c.option.TLSConfig != nil {
dialer := &net.Dialer{
Timeout: c.option.ConnectTimeout,
}
tlsConn, err = tls.DialWithDialer(dialer, "tcp", address, c.option.TLSConfig)
// or conn:= tls.Client(netConn, &config)
conn = net.Conn(tlsConn)
} else {
conn, err = net.DialTimeout("tcp", address, c.option.ConnectTimeout)
}
if err != nil {
log.Errorf("failed to dial server: %v", err)
return nil, err
}
_, err = io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")
if err != nil {
// Dial() success but Write() failed here, close the successfully
// created conn before return.
conn.Close()
log.Errorf("failed to make CONNECT: %v", err)
return nil, err
}
// Require successful HTTP response
// before switching to RPC protocol.
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
if err == nil && resp.Status == connected {
return conn, nil
}
if err == nil {
log.Errorf("unexpected HTTP response: %v", err)
err = errors.New("unexpected HTTP response: " + resp.Status)
}
conn.Close()
return nil, &net.OpError{
Op: "dial-http",
Net: network + " " + address,
Addr: nil,
Err: err,
}
}
func newDirectWSConn(c *Client, network, address string) (net.Conn, error) {
if c == nil {
return nil, errors.New("empty client")
}
path := c.option.RPCPath
if path == "" {
path = share.DefaultRPCPath
}
var conn net.Conn
var err error
// url := "ws://localhost:12345/ws"
var url, origin string
if network == "ws" {
url = fmt.Sprintf("ws://%s%s", address, path)
origin = fmt.Sprintf("http://%s", address)
} else {
url = fmt.Sprintf("wss://%s%s", address, path)
origin = fmt.Sprintf("https://%s", address)
}
if c.option.TLSConfig != nil {
config, erri := websocket.NewConfig(url, origin)
if erri != nil {
return nil, erri
}
config.TlsConfig = c.option.TLSConfig
conn, err = websocket.DialConfig(config)
} else {
conn, err = websocket.Dial(url, "", origin)
}
return conn, err
}
================================================
FILE: client/connection_iouring.go
================================================
package client
import (
"net"
)
// experimental
func newIOUringConn(c *Client, network, address string) (net.Conn, error) {
return newDirectConn(c, "tcp", address)
}
================================================
FILE: client/connection_iouring_test.go
================================================
//go:build linux
// +build linux
package client
// func TestXClient_IOUring(t *testing.T) {
// s := server.NewServer()
// s.RegisterName("Arith", new(Arith), "")
// go s.Serve("iouring", "127.0.0.1:8972")
// defer s.Close()
// time.Sleep(500 * time.Millisecond)
// addr := s.Address().String()
// d, err := NewPeer2PeerDiscovery("iouring@"+addr, "desc=a test service")
// if err != nil {
// t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err)
// }
// xclient := NewXClient("Arith", Failtry, RandomSelect, d, DefaultOption)
// defer xclient.Close()
// args := &Args{
// A: 10,
// B: 20,
// }
// reply := &Reply{}
// err = xclient.Call(context.Background(), "Mul", args, reply)
// if err != nil {
// t.Fatalf("failed to call: %v", err)
// }
// if reply.C != 200 {
// t.Fatalf("expect 200 but got %d", reply.C)
// }
// }
================================================
FILE: client/connection_kcp.go
================================================
// +build kcp
package client
import (
"net"
kcp "github.com/xtaci/kcp-go"
)
func newDirectKCPConn(c *Client, network, address string) (net.Conn, error) {
return kcp.DialWithOptions(address, c.option.Block.(kcp.BlockCrypt), 10, 3)
}
================================================
FILE: client/connection_memu.go
================================================
package client
import (
"net"
"github.com/akutz/memconn"
)
func newMemuConn(c *Client, network, address string) (net.Conn, error) {
return memconn.Dial(network, address)
}
================================================
FILE: client/connection_nonkcp.go
================================================
// +build !kcp
package client
import (
"errors"
"net"
)
func newDirectKCPConn(c *Client, network, address string) (net.Conn, error) {
return nil, errors.New("kcp unsupported")
}
================================================
FILE: client/connection_nonquic.go
================================================
//go:build !quic
// +build !quic
package client
import (
"errors"
"net"
)
func newDirectQuicConn(c *Client, network, address string) (net.Conn, error) {
return nil, errors.New("quic unsupported")
}
================================================
FILE: client/connection_quic.go
================================================
//go:build quic
// +build quic
package client
import (
"crypto/tls"
"net"
"github.com/quic-go/quic-go"
"github.com/smallnest/quick"
)
func newDirectQuicConn(c *Client, network, address string) (net.Conn, error) {
tlsConf := c.option.TLSConfig
if tlsConf == nil {
tlsConf = &tls.Config{InsecureSkipVerify: true}
}
if len(tlsConf.NextProtos) == 0 {
tlsConf.NextProtos = []string{"rpcx"}
}
quicConfig := &quic.Config{}
return quick.Dial(address, tlsConf, quicConfig)
}
================================================
FILE: client/connection_rdma.go
================================================
//go:build rdma
// +build rdma
package client
import (
"errors"
"net"
"github.com/smallnest/rsocket"
)
func init() {
ConnFactories["rdma"] = newRDMAConn
}
func newRDMAConn(c *Client, network, address string) (net.Conn, error) {
if network != "rdma" {
return nil, errors.New("network is not rdma")
}
return rsocket.DialTCP(address)
}
================================================
FILE: client/discovery.go
================================================
package client
import (
"encoding/json"
"os"
"path/filepath"
"sync"
)
// ServiceDiscoveryFilter can be used to filter services with customized logics.
// Servers can register its services but clients can use the customized filter to select some services.
// It returns true if ServiceDiscovery wants to use this service, otherwise it returns false.
type ServiceDiscoveryFilter func(kvp *KVPair) bool
// ServiceDiscovery defines ServiceDiscovery of zookeeper, etcd and consul
type ServiceDiscovery interface {
GetServices() []*KVPair // return all services in the registry
WatchService() chan []*KVPair // watch the change of services, it's a golang channel
RemoveWatcher(ch chan []*KVPair)
Clone(servicePath string) (ServiceDiscovery, error)
SetFilter(ServiceDiscoveryFilter) // set customized filter to filter services
Close()
}
type cachedServiceDiscovery struct {
threshold int
cachedFile string
cached []*KVPair
chansLock sync.RWMutex
chans map[chan []*KVPair]chan []*KVPair
ServiceDiscovery
}
// CacheDiscovery caches the services in a file, it will return the cached services if the number of services is greater than threshold.
// It is very useful when the register center is lost.
func CacheDiscovery(threshold int, cachedFile string, discovery ServiceDiscovery) ServiceDiscovery {
if cachedFile == "" {
cachedFile = ".cache/discovery.json"
}
cachedFileDir := filepath.Dir(cachedFile)
if _, err := os.Stat(cachedFileDir); os.IsNotExist(err) {
// 目录不存在,创建目录
if err := os.MkdirAll(cachedFileDir, os.ModePerm); err != nil {
panic(err)
}
}
return &cachedServiceDiscovery{
threshold: threshold,
cachedFile: cachedFile,
ServiceDiscovery: discovery,
chans: make(map[chan []*KVPair]chan []*KVPair),
}
}
func (cd *cachedServiceDiscovery) GetServices() []*KVPair {
kvPairs := cd.ServiceDiscovery.GetServices()
n := len(kvPairs)
if n > cd.threshold {
if n > len(cd.cached) { // strictly we should compare the content of the cached file, but only compare the length for performance
cd.cached = kvPairs
cd.storeCached(kvPairs)
}
return kvPairs
}
if len(cd.cached) == 0 {
cd.loadCached()
}
return cd.cached
}
func (cd *cachedServiceDiscovery) WatchService() chan []*KVPair {
ch := cd.ServiceDiscovery.WatchService()
cachedCh := make(chan []*KVPair, 10)
cd.chansLock.Lock()
cd.chans[cachedCh] = ch
cd.chansLock.Unlock()
go func() {
defer recover()
for {
kvPairs, ok := <-ch
if !ok {
close(cachedCh)
return
}
n := len(kvPairs)
if n > len(cd.cached) {
cd.cached = kvPairs
cd.storeCached(kvPairs)
}
cachedCh <- kvPairs
}
}()
return cachedCh
}
func (cd *cachedServiceDiscovery) RemoveWatcher(ch chan []*KVPair) {
cd.chansLock.Lock()
origin := cd.chans[ch]
delete(cd.chans, ch)
cd.chansLock.Unlock()
if origin != nil {
cd.ServiceDiscovery.RemoveWatcher(origin)
}
}
func (cd *cachedServiceDiscovery) storeCached(kvPairs []*KVPair) {
data, _ := json.Marshal(kvPairs)
os.WriteFile(cd.cachedFile, data, 0644)
}
func (cd *cachedServiceDiscovery) loadCached() (kvPairs []*KVPair) {
data, err := os.ReadFile(cd.cachedFile)
if err != nil || len(data) == 0 {
return
}
json.Unmarshal(data, &kvPairs)
return kvPairs
}
================================================
FILE: client/dns_discovery.go
================================================
package client
import (
"fmt"
"net"
"sort"
"sync"
"time"
"github.com/smallnest/rpcx/log"
)
// DNSDiscovery is based on DNS a record.
// You must set port and network info when you create the DNSDiscovery.
type DNSDiscovery struct {
domain string
network string
port int
d time.Duration
pairsMu sync.RWMutex
pairs []*KVPair
chans []chan []*KVPair
mu sync.Mutex
filter ServiceDiscoveryFilter
stopCh chan struct{}
}
// NewDNSDiscovery returns a new DNSDiscovery.
func NewDNSDiscovery(domain string, network string, port int, d time.Duration) (*DNSDiscovery, error) {
discovery := &DNSDiscovery{domain: domain, network: network, port: port, d: d}
discovery.lookup()
go discovery.watch()
return discovery, nil
}
// Clone clones this ServiceDiscovery with new servicePath.
func (d *DNSDiscovery) Clone(servicePath string) (ServiceDiscovery, error) {
return NewDNSDiscovery(d.domain, d.network, d.port, d.d)
}
// SetFilter sets the filer.
func (d *DNSDiscovery) SetFilter(filter ServiceDiscoveryFilter) {
d.filter = filter
}
// GetServices returns the static server
func (d *DNSDiscovery) GetServices() []*KVPair {
d.pairsMu.RLock()
defer d.pairsMu.RUnlock()
return d.pairs
}
// WatchService returns a nil chan.
func (d *DNSDiscovery) WatchService() chan []*KVPair {
d.mu.Lock()
defer d.mu.Unlock()
ch := make(chan []*KVPair, 10)
d.chans = append(d.chans, ch)
return ch
}
func (d *DNSDiscovery) RemoveWatcher(ch chan []*KVPair) {
d.mu.Lock()
defer d.mu.Unlock()
var chans []chan []*KVPair
for _, c := range d.chans {
if c == ch {
continue
}
chans = append(chans, c)
}
d.chans = chans
}
func (d *DNSDiscovery) lookup() {
var pairs []*KVPair // latest servers
ips, err := net.LookupIP(d.domain)
if err != nil {
log.Errorf("failed to lookup %s: %v", d.domain, err)
return
}
for _, ip := range ips {
pair := &KVPair{Key: fmt.Sprintf("%s@%s:%d", d.network, ip.String(), d.port)}
if d.filter != nil && !d.filter(pair) {
continue
}
pairs = append(pairs, pair)
}
if len(pairs) > 0 {
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Key < pairs[j].Key
})
}
d.pairsMu.Lock()
d.pairs = pairs
d.pairsMu.Unlock()
d.mu.Lock()
for _, ch := range d.chans {
ch := ch
go func() {
defer func() {
recover()
}()
select {
case ch <- pairs:
case <-time.After(time.Minute):
log.Warn("chan is full and new change has been dropped")
}
}()
}
d.mu.Unlock()
}
func (d *DNSDiscovery) watch() {
tick := time.NewTicker(d.d)
defer tick.Stop()
for {
select {
case <-d.stopCh:
return
case <-tick.C:
d.lookup()
}
}
}
func (d *DNSDiscovery) Close() {
close(d.stopCh)
}
================================================
FILE: client/failmode_enumer.go
================================================
// Code generated by "enumer -type=FailMode"; DO NOT EDIT.
package client
import (
"fmt"
)
const _FailModeName = "FailoverFailfastFailtryFailbackup"
var _FailModeIndex = [...]uint8{0, 8, 16, 23, 33}
func (i FailMode) String() string {
if i < 0 || i >= FailMode(len(_FailModeIndex)-1) {
return fmt.Sprintf("FailMode(%d)", i)
}
return _FailModeName[_FailModeIndex[i]:_FailModeIndex[i+1]]
}
var _FailModeValues = []FailMode{0, 1, 2, 3}
var _FailModeNameToValueMap = map[string]FailMode{
_FailModeName[0:8]: 0,
_FailModeName[8:16]: 1,
_FailModeName[16:23]: 2,
_FailModeName[23:33]: 3,
}
// FailModeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func FailModeString(s string) (FailMode, error) {
if val, ok := _FailModeNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to FailMode values", s)
}
// FailModeValues returns all values of the enum
func FailModeValues() []FailMode {
return _FailModeValues
}
// IsAFailMode returns "true" if the value is listed in the enum definition. "false" otherwise
func (i FailMode) IsAFailMode() bool {
for _, v := range _FailModeValues {
if i == v {
return true
}
}
return false
}
================================================
FILE: client/geo_utils.go
================================================
package client
import (
"math"
)
// https://gist.github.com/cdipaolo/d3f8db3848278b49db68
func getDistanceFrom(lat1, lon1, lat2, lon2 float64) float64 {
var la1, lo1, la2, lo2, r float64
la1 = lat1 * math.Pi / 180
lo1 = lon1 * math.Pi / 180
la2 = lat2 * math.Pi / 180
lo2 = lon2 * math.Pi / 180
r = 6378100 // Earth radius in METERS
// calculate
h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1)
return 2 * r * math.Asin(math.Sqrt(h))
}
func hsin(theta float64) float64 {
return math.Pow(math.Sin(theta/2), 2)
}
================================================
FILE: client/hash_utils.go
================================================
package client
import (
"fmt"
"hash/fnv"
)
// Hash consistently chooses a hash bucket number in the range [0, numBuckets) for the given key. numBuckets must be >= 1.
func Hash(key uint64, buckets int32) int32 {
if buckets <= 0 {
buckets = 1
}
var b, j int64
for j < int64(buckets) {
b = j
key = key*2862933555777941757 + 1
j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
}
return int32(b)
}
// HashString get a hash value of a string
func HashString(s string) uint64 {
h := fnv.New64a()
h.Write([]byte(s))
return h.Sum64()
}
// HashServiceAndArgs define a hash function
type HashServiceAndArgs func(len int, options ...interface{}) int
// ConsistentFunction define a hash function
// Return service address, like "tcp@127.0.0.1:8970"
type ConsistentAddrStrFunction func(options ...interface{}) string
func genKey(options ...interface{}) uint64 {
keyString := ""
for _, opt := range options {
keyString = keyString + "/" + toString(opt)
}
return HashString(keyString)
}
// JumpConsistentHash selects a server by serviceMethod and args
func JumpConsistentHash(len int, options ...interface{}) int {
return int(Hash(genKey(options...), int32(len)))
}
func toString(obj interface{}) string {
return fmt.Sprintf("%v", obj)
}
================================================
FILE: client/mdns_discovery.go
================================================
package client
import (
"context"
"encoding/json"
"net/url"
"sync"
"time"
"github.com/grandcat/zeroconf"
"github.com/smallnest/rpcx/log"
)
type serviceMeta struct {
Service string `json:"service,omitempty"`
Meta string `json:"meta,omitempty"`
ServiceAddress string `json:"service_address,omitempty"`
}
// MDNSDiscovery is a mdns service discovery.
// It always returns the registered servers in mdns.
type MDNSDiscovery struct {
Timeout time.Duration
WatchInterval time.Duration
domain string
service string
pairsMu sync.RWMutex
pairs []*KVPair
chans []chan []*KVPair
mu sync.Mutex
filter ServiceDiscoveryFilter
stopCh chan struct{}
}
// NewMDNSDiscovery returns a new MDNSDiscovery.
// If domain is empty, use "local." in default.
func NewMDNSDiscovery(service string, timeout time.Duration, watchInterval time.Duration, domain string) (*MDNSDiscovery, error) {
if domain == "" {
domain = "local."
}
d := &MDNSDiscovery{service: service, Timeout: timeout, WatchInterval: watchInterval, domain: domain}
d.stopCh = make(chan struct{})
var err error
d.pairsMu.Lock()
d.pairs, err = d.browse()
d.pairsMu.Unlock()
if err != nil {
log.Warnf("failed to browse services: %v", err)
}
go d.watch()
return d, nil
}
// Clone clones this ServiceDiscovery with new servicePath.
func (d *MDNSDiscovery) Clone(servicePath string) (ServiceDiscovery, error) {
return NewMDNSDiscovery(servicePath, d.Timeout, d.WatchInterval, d.domain)
}
// SetFilter sets the filer.
func (d *MDNSDiscovery) SetFilter(filter ServiceDiscoveryFilter) {
d.filter = filter
}
// GetServices returns the servers
func (d *MDNSDiscovery) GetServices() []*KVPair {
d.pairsMu.RLock()
defer d.pairsMu.RUnlock()
return d.pairs
}
// WatchService returns a nil chan.
func (d *MDNSDiscovery) WatchService() chan []*KVPair {
d.mu.Lock()
defer d.mu.Unlock()
ch := make(chan []*KVPair, 10)
d.chans = append(d.chans, ch)
return ch
}
func (d *MDNSDiscovery) RemoveWatcher(ch chan []*KVPair) {
d.mu.Lock()
defer d.mu.Unlock()
var chans []chan []*KVPair
for _, c := range d.chans {
if c == ch {
continue
}
chans = append(chans, c)
}
d.chans = chans
}
func (d *MDNSDiscovery) watch() {
t := time.NewTicker(d.WatchInterval)
for {
select {
case <-d.stopCh:
t.Stop()
log.Info("discovery has been closed")
return
case <-t.C:
pairs, err := d.browse()
if err == nil {
d.pairsMu.Lock()
d.pairs = pairs
d.pairsMu.Unlock()
d.mu.Lock()
for _, ch := range d.chans {
ch := ch
go func() {
defer func() {
recover()
}()
select {
case ch <- pairs:
case <-time.After(time.Minute):
log.Warn("chan is full and new change has ben dropped")
}
}()
}
d.mu.Unlock()
}
}
}
}
func (d *MDNSDiscovery) browse() ([]*KVPair, error) {
resolver, err := zeroconf.NewResolver(nil)
if err != nil {
log.Warnf("Failed to initialize resolver: %v", err)
return nil, err
}
entries := make(chan *zeroconf.ServiceEntry)
var totalServices []*KVPair
var services []*serviceMeta
done := make(chan struct{})
go func(results <-chan *zeroconf.ServiceEntry) {
for entry := range entries {
s, _ := url.QueryUnescape(entry.Text[0])
err := json.Unmarshal([]byte(s), &services)
if err != nil {
log.Warnf("Failed to browse: %v", err)
continue
}
for _, sm := range services {
pair := &KVPair{
Key: sm.ServiceAddress,
Value: sm.Meta,
}
if d.filter != nil && !d.filter(pair) {
continue
}
totalServices = append(totalServices, pair)
}
}
close(done)
}(entries)
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
defer cancel()
err = resolver.Browse(ctx, "_rpcxservices", d.domain, entries)
if err != nil {
log.Warnf("Failed to browse: %v", err)
}
<-done
return totalServices, nil
}
func (d *MDNSDiscovery) Close() {
close(d.stopCh)
}
================================================
FILE: client/mode.go
================================================
package client
// FailMode decides how clients action when clients fail to invoke services
type FailMode int
const (
// Failover selects another server automaticaly
Failover FailMode = iota
// Failfast returns error immediately
Failfast
// Failtry use current client again
Failtry
// Failbackup select another server if the first server doesn't respond in specified time and use the fast response.
Failbackup
)
// SelectMode defines the algorithm of selecting a services from candidates.
type SelectMode int
const (
// RandomSelect is selecting randomly
RandomSelect SelectMode = iota
// RoundRobin is selecting by round robin
RoundRobin
// WeightedRoundRobin is selecting by weighted round robin
WeightedRoundRobin
// WeightedICMP is selecting by weighted Ping time
WeightedICMP
// ConsistentHash is selecting by hashing
ConsistentHash
// Closest is selecting the closest server
Closest
// SelectByUser is selecting by implementation of users
SelectByUser = 1000
)
================================================
FILE: client/multiple_servers_discovery.go
================================================
package client
import (
"sync"
"time"
"github.com/smallnest/rpcx/log"
)
// MultipleServersDiscovery is a multiple servers service discovery.
// It always returns the current servers and users can change servers dynamically.
type MultipleServersDiscovery struct {
pairsMu sync.RWMutex
pairs []*KVPair
chans []chan []*KVPair
mu sync.Mutex
}
// NewMultipleServersDiscovery returns a new MultipleServersDiscovery.
func NewMultipleServersDiscovery(pairs []*KVPair) (*MultipleServersDiscovery, error) {
return &MultipleServersDiscovery{
pairs: pairs,
}, nil
}
// Clone clones this ServiceDiscovery with new servicePath.
func (d *MultipleServersDiscovery) Clone(servicePath string) (ServiceDiscovery, error) {
return d, nil
}
// SetFilter sets the filer.
func (d *MultipleServersDiscovery) SetFilter(filter ServiceDiscoveryFilter) {
}
// GetServices returns the configured server
func (d *MultipleServersDiscovery) GetServices() []*KVPair {
d.pairsMu.RLock()
defer d.pairsMu.RUnlock()
return d.pairs
}
// WatchService returns a nil chan.
func (d *MultipleServersDiscovery) WatchService() chan []*KVPair {
d.mu.Lock()
defer d.mu.Unlock()
ch := make(chan []*KVPair, 10)
d.chans = append(d.chans, ch)
return ch
}
func (d *MultipleServersDiscovery) RemoveWatcher(ch chan []*KVPair) {
d.mu.Lock()
defer d.mu.Unlock()
var chans []chan []*KVPair
for _, c := range d.chans {
if c == ch {
continue
}
chans = append(chans, c)
}
d.chans = chans
}
// Update is used to update servers at runtime.
func (d *MultipleServersDiscovery) Update(pairs []*KVPair) {
d.mu.Lock()
defer d.mu.Unlock()
for _, ch := range d.chans {
ch := ch
go func() {
defer func() {
recover()
}()
select {
case ch <- pairs:
case <-time.After(time.Minute):
log.Warn("chan is full and new change has been dropped")
}
}()
}
d.pairsMu.Lock()
d.pairs = pairs
d.pairsMu.Unlock()
}
func (d *MultipleServersDiscovery) Close() {
}
================================================
FILE: client/oneclient.go
================================================
package client
import (
"context"
"fmt"
"io"
"net"
"sync"
"github.com/smallnest/rpcx/share"
multierror "github.com/hashicorp/go-multierror"
"github.com/smallnest/rpcx/protocol"
)
// OneClient wraps servicesPath and XClients.
// Users can use a shared oneclient to access multiple services.
type OneClient struct {
xclients map[string]XClient
mu sync.RWMutex
failMode FailMode
selectMode SelectMode
discovery ServiceDiscovery
option Option
selectors map[string]Selector
Plugins PluginContainer
latitude float64
longitude float64
auth string
serverMessageChan chan<- *protocol.Message
}
// NewOneClient creates a OneClient that supports service discovery and service governance.
func NewOneClient(failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) *OneClient {
return &OneClient{
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
option: option,
xclients: make(map[string]XClient),
selectors: make(map[string]Selector),
}
}
// NewBidirectionalOneClient creates a new xclient that can receive notifications from servers.
func NewBidirectionalOneClient(failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) *OneClient {
return &OneClient{
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
option: option,
xclients: make(map[string]XClient),
selectors: make(map[string]Selector),
serverMessageChan: serverMessageChan,
}
}
// SetSelector sets customized selector by users.
func (c *OneClient) SetSelector(servicePath string, s Selector) {
c.mu.Lock()
c.selectors[servicePath] = s
if xclient, ok := c.xclients[servicePath]; ok {
xclient.SetSelector(s)
}
c.mu.Unlock()
}
// SetPlugins sets client's plugins.
func (c *OneClient) SetPlugins(plugins PluginContainer) {
c.Plugins = plugins
c.mu.RLock()
for _, v := range c.xclients {
v.SetPlugins(plugins)
}
c.mu.RUnlock()
}
func (c *OneClient) GetPlugins() PluginContainer {
return c.Plugins
}
// ConfigGeoSelector sets location of client's latitude and longitude,
// and use newGeoSelector.
func (c *OneClient) ConfigGeoSelector(latitude, longitude float64) {
c.selectMode = Closest
c.latitude = latitude
c.longitude = longitude
c.mu.RLock()
for _, v := range c.xclients {
v.ConfigGeoSelector(latitude, longitude)
}
c.mu.RUnlock()
}
// Auth sets s token for Authentication.
func (c *OneClient) Auth(auth string) {
c.auth = auth
c.mu.RLock()
for _, v := range c.xclients {
v.Auth(auth)
}
c.mu.RUnlock()
}
// Go invokes the function asynchronously. It returns the Call structure representing the invocation. The done channel will signal when the call is complete by returning the same Call object. If done is nil, Go will allocate a new channel. If non-nil, done must be buffered or Go will deliberately crash.
// It does not use FailMode.
func (c *OneClient) Go(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error) {
c.mu.RLock()
xclient := c.xclients[servicePath]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[servicePath]
if xclient == nil {
xclient, err = c.newXClient(servicePath)
c.xclients[servicePath] = xclient
}
c.mu.Unlock()
if err != nil {
return nil, err
}
}
return xclient.Go(ctx, serviceMethod, args, reply, done)
}
func (c *OneClient) newXClient(servicePath string) (xclient XClient, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
err = e
} else {
err = fmt.Errorf("%v", r)
}
}
}()
d, err := c.discovery.Clone(servicePath)
if err != nil {
return nil, err
}
if c.serverMessageChan == nil {
xclient = NewXClient(servicePath, c.failMode, c.selectMode, d, c.option)
} else {
xclient = NewBidirectionalXClient(servicePath, c.failMode, c.selectMode, d, c.option, c.serverMessageChan)
}
if c.Plugins != nil {
xclient.SetPlugins(c.Plugins)
}
if s, ok := c.selectors[servicePath]; ok {
xclient.SetSelector(s)
}
if c.selectMode == Closest {
xclient.ConfigGeoSelector(c.latitude, c.longitude)
}
if c.auth != "" {
xclient.Auth(c.auth)
}
return xclient, err
}
// Call invokes the named function, waits for it to complete, and returns its error status.
// It handles errors base on FailMode.
func (c *OneClient) Call(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}) error {
c.mu.RLock()
xclient := c.xclients[servicePath]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[servicePath]
if xclient == nil {
xclient, err = c.newXClient(servicePath)
c.xclients[servicePath] = xclient
}
c.mu.Unlock()
if err != nil {
return err
}
}
return xclient.Call(ctx, serviceMethod, args, reply)
}
func (c *OneClient) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) {
servicePath := r.ServicePath
c.mu.RLock()
xclient := c.xclients[servicePath]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[servicePath]
if xclient == nil {
xclient, err = c.newXClient(servicePath)
c.xclients[servicePath] = xclient
}
c.mu.Unlock()
if err != nil {
return nil, nil, err
}
}
return xclient.SendRaw(ctx, r)
}
// Broadcast sends requests to all servers and Success only when all servers return OK.
// FailMode and SelectMode are meanless for this method.
// Please set timeout to avoid hanging.
func (c *OneClient) Broadcast(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}) error {
c.mu.RLock()
xclient := c.xclients[servicePath]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[servicePath]
if xclient == nil {
xclient, err = c.newXClient(servicePath)
c.xclients[servicePath] = xclient
}
c.mu.Unlock()
if err != nil {
return err
}
}
return xclient.Broadcast(ctx, serviceMethod, args, reply)
}
// Fork sends requests to all servers and Success once one server returns OK.
// FailMode and SelectMode are meanless for this method.
func (c *OneClient) Fork(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}) error {
c.mu.RLock()
xclient := c.xclients[servicePath]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[servicePath]
if xclient == nil {
xclient, err = c.newXClient(servicePath)
c.xclients[servicePath] = xclient
}
c.mu.Unlock()
if err != nil {
return err
}
}
return xclient.Fork(ctx, serviceMethod, args, reply)
}
func (c *OneClient) SendFile(ctx context.Context, fileName string, rateInBytesPerSecond int64, meta map[string]string) error {
c.mu.RLock()
xclient := c.xclients[share.SendFileServiceName]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[share.SendFileServiceName]
if xclient == nil {
xclient, err = c.newXClient(share.SendFileServiceName)
c.xclients[share.SendFileServiceName] = xclient
}
c.mu.Unlock()
if err != nil {
return err
}
}
return xclient.SendFile(ctx, fileName, rateInBytesPerSecond, meta)
}
func (c *OneClient) DownloadFile(ctx context.Context, requestFileName string, saveTo io.Writer, meta map[string]string) error {
c.mu.RLock()
xclient := c.xclients[share.SendFileServiceName]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[share.SendFileServiceName]
if xclient == nil {
xclient, err = c.newXClient(share.SendFileServiceName)
c.xclients[share.SendFileServiceName] = xclient
}
c.mu.Unlock()
if err != nil {
return err
}
}
return xclient.DownloadFile(ctx, requestFileName, saveTo, meta)
}
func (c *OneClient) Stream(ctx context.Context, meta map[string]string) (net.Conn, error) {
c.mu.RLock()
xclient := c.xclients[share.StreamServiceName]
c.mu.RUnlock()
if xclient == nil {
var err error
c.mu.Lock()
xclient = c.xclients[share.StreamServiceName]
if xclient == nil {
xclient, err = c.newXClient(share.StreamServiceName)
c.xclients[share.StreamServiceName] = xclient
}
c.mu.Unlock()
if err != nil {
return nil, err
}
}
return xclient.Stream(ctx, meta)
}
// Close closes all xclients and its underlying connections to services.
func (c *OneClient) Close() error {
var result error
c.mu.RLock()
for _, v := range c.xclients {
err := v.Close()
if err != nil {
result = multierror.Append(result, err)
}
}
c.mu.RUnlock()
return result
}
================================================
FILE: client/oneclient_pool.go
================================================
package client
import (
"sync/atomic"
"github.com/smallnest/rpcx/protocol"
)
// OneClientPool is a oneclient pool with fixed size.
// It uses roundrobin algorithm to call its xclients.
// All oneclients share the same configurations such as ServiceDiscovery and serverMessageChan.
type OneClientPool struct {
count uint64
index uint64
oneclients []*OneClient
auth string
Plugins PluginContainer
failMode FailMode
selectMode SelectMode
discovery ServiceDiscovery
option Option
serverMessageChan chan<- *protocol.Message
}
// NewOneClientPool creates a fixed size OneClient pool.
func NewOneClientPool(count int, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) *OneClientPool {
pool := &OneClientPool{
count: uint64(count),
oneclients: make([]*OneClient, count),
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
option: option,
}
for i := 0; i < count; i++ {
oneclient := NewOneClient(failMode, selectMode, discovery, option)
pool.oneclients[i] = oneclient
}
return pool
}
// NewBidirectionalOneClientPool creates a BidirectionalOneClient pool with fixed size.
func NewBidirectionalOneClientPool(count int, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) *OneClientPool {
pool := &OneClientPool{
count: uint64(count),
oneclients: make([]*OneClient, count),
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
option: option,
serverMessageChan: serverMessageChan,
}
for i := 0; i < count; i++ {
oneclient := NewBidirectionalOneClient(failMode, selectMode, discovery, option, serverMessageChan)
pool.oneclients[i] = oneclient
}
return pool
}
// Auth sets s token for Authentication.
func (p *OneClientPool) Auth(auth string) {
p.auth = auth
for _, v := range p.oneclients {
v.Auth(auth)
}
}
// SetPlugins sets client's plugins.
func (p *OneClientPool) SetPlugins(plugins PluginContainer) {
p.Plugins = plugins
for _, v := range p.oneclients {
v.SetPlugins(plugins)
}
}
// GetPlugins returns client's plugins.
func (p *OneClientPool) GetPlugins() PluginContainer {
return p.Plugins
}
// Get returns a OneClient.
// It does not remove this OneClient from its cache so you don't need to put it back.
// Don't close this OneClient because maybe other goroutines are using this OneClient.
func (p *OneClientPool) Get() *OneClient {
i := atomic.AddUint64(&p.index, 1)
picked := int(i % p.count)
return p.oneclients[picked]
}
// Close this pool.
// Please make sure it won't be used any more.
func (p *OneClientPool) Close() {
for _, c := range p.oneclients {
c.Close()
}
p.oneclients = nil
}
================================================
FILE: client/oneclient_pool_test.go
================================================
package client
import (
"testing"
)
func TestOneClientPool_SetPlugins(t *testing.T) {
// Create a simple discovery
pairs := []*KVPair{
{Key: "tcp@127.0.0.1:8972", Value: ""},
}
discovery, err := NewMultipleServersDiscovery(pairs)
if err != nil {
t.Fatalf("failed to create discovery: %v", err)
}
defer discovery.Close()
// Create a pool
pool := NewOneClientPool(3, Failtry, RandomSelect, discovery, DefaultOption)
defer pool.Close()
// Create plugins
plugins := NewPluginContainer()
tp := &testPlugin{name: "test-plugin"}
plugins.Add(tp)
// Test SetPlugins
pool.SetPlugins(plugins)
// Verify plugins are set on pool
if pool.GetPlugins() == nil {
t.Error("plugins should not be nil after SetPlugins")
}
if pool.GetPlugins() != plugins {
t.Error("pool plugins should be the same as the set plugins")
}
// Verify plugins are set on all oneclients
for i := 0; i < 3; i++ {
oneclient := pool.Get()
if oneclient.GetPlugins() == nil {
t.Errorf("oneclient %d plugins should not be nil", i)
}
if oneclient.GetPlugins() != plugins {
t.Errorf("oneclient %d plugins should be the same as the set plugins", i)
}
}
}
func TestOneClientPool_GetPlugins(t *testing.T) {
// Create a simple discovery
pairs := []*KVPair{
{Key: "tcp@127.0.0.1:8972", Value: ""},
}
discovery, err := NewMultipleServersDiscovery(pairs)
if err != nil {
t.Fatalf("failed to create discovery: %v", err)
}
defer discovery.Close()
// Create a pool
pool := NewOneClientPool(2, Failtry, RandomSelect, discovery, DefaultOption)
defer pool.Close()
// Initially, plugins should be nil
if pool.GetPlugins() != nil {
t.Error("plugins should be nil initially")
}
// Create and set plugins
plugins := NewPluginContainer()
tp := &testPlugin{name: "test-plugin"}
plugins.Add(tp)
pool.SetPlugins(plugins)
// Verify GetPlugins returns the correct plugins
retrievedPlugins := pool.GetPlugins()
if retrievedPlugins == nil {
t.Error("plugins should not be nil after SetPlugins")
}
if retrievedPlugins != plugins {
t.Error("GetPlugins should return the same plugins as SetPlugins")
}
// Verify plugins contain the test plugin
allPlugins := retrievedPlugins.All()
if len(allPlugins) != 1 {
t.Errorf("expected 1 plugin, got %d", len(allPlugins))
}
if p, ok := allPlugins[0].(*testPlugin); !ok || p.name != "test-plugin" {
t.Error("plugin should be the test plugin")
}
}
func TestOneClientPool_SetPlugins_Concurrent(t *testing.T) {
// Create a simple discovery
pairs := []*KVPair{
{Key: "tcp@127.0.0.1:8972", Value: ""},
}
discovery, err := NewMultipleServersDiscovery(pairs)
if err != nil {
t.Fatalf("failed to create discovery: %v", err)
}
defer discovery.Close()
// Create a pool
pool := NewOneClientPool(5, Failtry, RandomSelect, discovery, DefaultOption)
defer pool.Close()
// Test concurrent SetPlugins calls
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(id int) {
plugins := NewPluginContainer()
tp := &testPlugin{name: "test-plugin"}
plugins.Add(tp)
pool.SetPlugins(plugins)
done <- true
}(i)
}
// Wait for all goroutines to complete
for i := 0; i < 10; i++ {
<-done
}
// Verify plugins are set
if pool.GetPlugins() == nil {
t.Error("plugins should not be nil after concurrent SetPlugins")
}
}
================================================
FILE: client/peer2peer_discovery.go
================================================
package client
// Peer2PeerDiscovery is a peer-to-peer service discovery.
// It always returns the static server.
type Peer2PeerDiscovery struct {
server string
metadata string
}
// NewPeer2PeerDiscovery returns a new Peer2PeerDiscovery.
func NewPeer2PeerDiscovery(server, metadata string) (*Peer2PeerDiscovery, error) {
return &Peer2PeerDiscovery{server: server, metadata: metadata}, nil
}
// Clone clones this ServiceDiscovery with new servicePath.
func (d *Peer2PeerDiscovery) Clone(servicePath string) (ServiceDiscovery, error) {
return d, nil
}
// SetFilter sets the filer.
func (d *Peer2PeerDiscovery) SetFilter(filter ServiceDiscoveryFilter) {
}
// GetServices returns the static server
func (d *Peer2PeerDiscovery) GetServices() []*KVPair {
return []*KVPair{&KVPair{Key: d.server, Value: d.metadata}}
}
// WatchService returns a nil chan.
func (d *Peer2PeerDiscovery) WatchService() chan []*KVPair {
return nil
}
func (d *Peer2PeerDiscovery) RemoveWatcher(ch chan []*KVPair) {}
func (d *Peer2PeerDiscovery) Close() {
}
================================================
FILE: client/ping_utils.go
================================================
package client
import (
"context"
"net"
"strings"
"time"
ping "github.com/go-ping/ping"
)
// weightedICMPSelector selects servers with ping result.
type weightedICMPSelector struct {
servers []*Weighted
wrs *weightedRoundRobinSelector
}
func newWeightedICMPSelector(servers map[string]string) Selector {
ss := createICMPWeighted(servers)
wicmps := weightedICMPSelector{
servers: ss,
wrs: &weightedRoundRobinSelector{servers: ss},
}
wicmps.wrs.servers = ss
wicmps.wrs.buildRing()
return &wicmps
}
func (s *weightedICMPSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
return s.wrs.Select(ctx, servicePath, serviceMethod, args)
}
func (s *weightedICMPSelector) UpdateServer(servers map[string]string) {
ss := createICMPWeighted(servers)
s.wrs.servers = ss
s.servers = ss
s.wrs.buildRing()
}
func createICMPWeighted(servers map[string]string) []*Weighted {
var ss = make([]*Weighted, 0, len(servers))
for k := range servers {
w := &Weighted{Server: k, Weight: 1, EffectiveWeight: 1}
server := strings.Split(k, "@")
host, _, _ := net.SplitHostPort(server[1])
rtt, _ := Ping(host)
rtt = CalculateWeight(rtt)
w.Weight = rtt
w.EffectiveWeight = rtt
ss = append(ss, w)
}
return ss
}
// Ping gets network traffic by ICMP
func Ping(host string) (rtt int, err error) {
rtt = 1000 // default and timeout is 1000 ms
pinger, err := ping.NewPinger(host)
if err != nil {
return rtt, err
}
pinger.Count = 3
pinger.Timeout = 3 * time.Second
err = pinger.Run()
if err != nil {
return rtt, err
}
stats := pinger.Statistics()
// ping failed
if len(stats.Rtts) == 0 {
return rtt, err
}
rtt = int(stats.AvgRtt) / 1e6
return rtt, err
}
// CalculateWeight converts the rtt to weighted by:
// 1. weight=191 if t <= 10
// 2. weight=201 -t if 10 < t <=200
// 3. weight=1 if 200 < t < 1000
// 4. weight = 0 if t >= 1000
//
// It means servers that ping time t < 10 will be preferred
// and servers won't be selected if t > 1000.
// It is hard coded based on Ops experience.
func CalculateWeight(rtt int) int {
switch {
case rtt >= 0 && rtt <= 10:
return 191
case rtt > 10 && rtt <= 200:
return 201 - rtt
case rtt > 100 && rtt < 1000:
return 1
default:
return 0
}
}
================================================
FILE: client/plugin.go
================================================
package client
import (
"context"
"net"
"github.com/smallnest/rpcx/protocol"
)
// pluginContainer implements PluginContainer interface.
type pluginContainer struct {
plugins []Plugin
}
func NewPluginContainer() PluginContainer {
return &pluginContainer{}
}
// Plugin is the client plugin interface.
type Plugin interface{}
// Add adds a plugin.
func (p *pluginContainer) Add(plugin Plugin) {
p.plugins = append(p.plugins, plugin)
}
// Remove removes a plugin by its name.
func (p *pluginContainer) Remove(plugin Plugin) {
if p.plugins == nil {
return
}
var plugins []Plugin
for _, pp := range p.plugins {
if pp != plugin {
plugins = append(plugins, pp)
}
}
p.plugins = plugins
}
// All returns all plugins
func (p *pluginContainer) All() []Plugin {
return p.plugins
}
// DoPreCall executes before call
func (p *pluginContainer) DoPreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PreCallPlugin); ok {
err := plugin.PreCall(ctx, servicePath, serviceMethod, args)
if err != nil {
return err
}
}
}
return nil
}
// DoPostCall executes after call
func (p *pluginContainer) DoPostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostCallPlugin); ok {
err = plugin.PostCall(ctx, servicePath, serviceMethod, args, reply, err)
if err != nil {
return err
}
}
}
return nil
}
// DoConnCreated is called in case of client connection created.
func (p *pluginContainer) DoConnCreated(conn net.Conn) (net.Conn, error) {
var err error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(ConnCreatedPlugin); ok {
conn, err = plugin.ConnCreated(conn)
if err != nil {
return conn, err
}
}
}
return conn, nil
}
// DoConnCreateFailed is called in case of client connection create failed.
func (p *pluginContainer) DoConnCreateFailed(network, address string) {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(ConnCreateFailedPlugin); ok {
plugin.ConnCreateFailed(network, address)
}
}
}
// DoClientConnected is called in case of connected.
func (p *pluginContainer) DoClientConnected(conn net.Conn) (net.Conn, error) {
var err error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(ClientConnectedPlugin); ok {
conn, err = plugin.ClientConnected(conn)
if err != nil {
return conn, err
}
}
}
return conn, nil
}
// DoClientConnectionClose is called in case of connection close.
func (p *pluginContainer) DoClientConnectionClose(conn net.Conn) error {
var err error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(ClientConnectionClosePlugin); ok {
err = plugin.ClientConnectionClose(conn)
if err != nil {
return err
}
}
}
return err
}
// DoClientBeforeEncode is called when requests are encoded and sent.
func (p *pluginContainer) DoClientBeforeEncode(req *protocol.Message) error {
var err error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(ClientBeforeEncodePlugin); ok {
err = plugin.ClientBeforeEncode(req)
if err != nil {
return err
}
}
}
return nil
}
// DoClientAfterDecode is called when requests are decoded and received.
func (p *pluginContainer) DoClientAfterDecode(req *protocol.Message) error {
var err error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(ClientAfterDecodePlugin); ok {
err = plugin.ClientAfterDecode(req)
if err != nil {
return err
}
}
}
return nil
}
// DoWrapSelect is called when select a node.
func (p *pluginContainer) DoWrapSelect(fn SelectFunc) SelectFunc {
rt := fn
for i := range p.plugins {
if pn, ok := p.plugins[i].(SelectNodePlugin); ok {
rt = pn.WrapSelect(rt)
}
}
return rt
}
type (
// PreCallPlugin is invoked before the client calls a server.
PreCallPlugin interface {
PreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error
}
// PostCallPlugin is invoked after the client calls a server.
PostCallPlugin interface {
PostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error
}
// ConnCreatedPlugin is invoked when the client connection has created.
ConnCreatedPlugin interface {
ConnCreated(net.Conn) (net.Conn, error)
}
ConnCreateFailedPlugin interface {
ConnCreateFailed(network, address string)
}
// ClientConnectedPlugin is invoked when the client has connected the server.
ClientConnectedPlugin interface {
ClientConnected(net.Conn) (net.Conn, error)
}
// ClientConnectionClosePlugin is invoked when the connection is closing.
ClientConnectionClosePlugin interface {
ClientConnectionClose(net.Conn) error
}
// ClientBeforeEncodePlugin is invoked when the message is encoded and sent.
ClientBeforeEncodePlugin interface {
ClientBeforeEncode(*protocol.Message) error
}
// ClientAfterDecodePlugin is invoked when the message is decoded.
ClientAfterDecodePlugin interface {
ClientAfterDecode(*protocol.Message) error
}
// SelectNodePlugin can interrupt selecting of xclient and add customized logics such as skipping some nodes.
SelectNodePlugin interface {
WrapSelect(SelectFunc) SelectFunc
}
// PluginContainer represents a plugin container that defines all methods to manage plugins.
// And it also defines all extension points.
PluginContainer interface {
Add(plugin Plugin)
Remove(plugin Plugin)
All() []Plugin
DoConnCreated(net.Conn) (net.Conn, error)
DoConnCreateFailed(network, address string)
DoClientConnected(net.Conn) (net.Conn, error)
DoClientConnectionClose(net.Conn) error
DoPreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error
DoPostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error
DoClientBeforeEncode(*protocol.Message) error
DoClientAfterDecode(*protocol.Message) error
DoWrapSelect(SelectFunc) SelectFunc
}
)
================================================
FILE: client/selectmode_enumer.go
================================================
// Code generated by "enumer -type=SelectMode"; DO NOT EDIT.
package client
import (
"fmt"
)
const _SelectModeName = "RandomSelectRoundRobinWeightedRoundRobinWeightedICMPConsistentHashClosest"
var _SelectModeIndex = [...]uint8{0, 12, 22, 40, 52, 66, 73}
func (i SelectMode) String() string {
if i < 0 || i >= SelectMode(len(_SelectModeIndex)-1) {
return fmt.Sprintf("SelectMode(%d)", i)
}
return _SelectModeName[_SelectModeIndex[i]:_SelectModeIndex[i+1]]
}
var _SelectModeValues = []SelectMode{0, 1, 2, 3, 4, 5}
var _SelectModeNameToValueMap = map[string]SelectMode{
_SelectModeName[0:12]: 0,
_SelectModeName[12:22]: 1,
_SelectModeName[22:40]: 2,
_SelectModeName[40:52]: 3,
_SelectModeName[52:66]: 4,
_SelectModeName[66:73]: 5,
}
// SelectModeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func SelectModeString(s string) (SelectMode, error) {
if val, ok := _SelectModeNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to SelectMode values", s)
}
// SelectModeValues returns all values of the enum
func SelectModeValues() []SelectMode {
return _SelectModeValues
}
// IsASelectMode returns "true" if the value is listed in the enum definition. "false" otherwise
func (i SelectMode) IsASelectMode() bool {
for _, v := range _SelectModeValues {
if i == v {
return true
}
}
return false
}
================================================
FILE: client/selector.go
================================================
package client
import (
"container/ring"
"context"
"math"
"math/rand"
"net/url"
"sort"
"strconv"
"time"
"github.com/edwingeng/doublejump"
"github.com/valyala/fastrand"
)
type SelectFunc func(ctx context.Context, servicePath, serviceMethod string, args interface{}) string
// Selector defines selector that selects one service from candidates.
type Selector interface {
Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string // SelectFunc
UpdateServer(servers map[string]string)
}
func newSelector(selectMode SelectMode, servers map[string]string) Selector {
switch selectMode {
case RandomSelect:
return newRandomSelector(servers)
case RoundRobin:
return newRoundRobinSelector(servers)
case WeightedRoundRobin:
return newWeightedRoundRobinSelector(servers)
case WeightedICMP:
return newWeightedICMPSelector(servers)
case ConsistentHash:
return newConsistentHashSelector(servers)
case SelectByUser:
return nil
default:
return newRandomSelector(servers)
}
}
// randomSelector selects randomly.
type randomSelector struct {
servers []string
}
func newRandomSelector(servers map[string]string) Selector {
ss := make([]string, 0, len(servers))
for k := range servers {
ss = append(ss, k)
}
return &randomSelector{servers: ss}
}
func (s *randomSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
ss := s.servers
if len(ss) == 0 {
return ""
}
i := fastrand.Uint32n(uint32(len(ss)))
return ss[i]
}
func (s *randomSelector) UpdateServer(servers map[string]string) {
ss := make([]string, 0, len(servers))
for k := range servers {
ss = append(ss, k)
}
s.servers = ss
}
// roundRobinSelector selects servers with roundrobin.
type roundRobinSelector struct {
servers []string
i int
}
func newRoundRobinSelector(servers map[string]string) Selector {
ss := make([]string, 0, len(servers))
for k := range servers {
ss = append(ss, k)
}
return &roundRobinSelector{servers: ss}
}
func (s *roundRobinSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
ss := s.servers
if len(ss) == 0 {
return ""
}
i := s.i
i = i % len(ss)
s.i = i + 1
return ss[i]
}
func (s *roundRobinSelector) UpdateServer(servers map[string]string) {
ss := make([]string, 0, len(servers))
for k := range servers {
ss = append(ss, k)
}
s.servers = ss
}
// weightedRoundRobinSelector selects servers with weighted.
type weightedRoundRobinSelector struct {
servers []*Weighted
totalWeight int
rr *ring.Ring
}
func newWeightedRoundRobinSelector(servers map[string]string) Selector {
ss := createWeighted(servers)
s := &weightedRoundRobinSelector{servers: ss}
s.buildRing()
return s
}
func (s *weightedRoundRobinSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
ss := s.servers
if len(ss) == 0 {
return ""
}
val := s.rr.Value
s.rr = s.rr.Next()
return val.(*Weighted).Server
}
func (s *weightedRoundRobinSelector) UpdateServer(servers map[string]string) {
newServer := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector)
*s = *newServer
}
func (s *weightedRoundRobinSelector) buildRing() {
s.totalWeight = 0
for _, w := range s.servers {
s.totalWeight += w.Weight
}
s.rr = ring.New(s.totalWeight)
for i := 0; i < s.totalWeight; i++ {
n := s.next()
s.rr.Value = n
s.rr = s.rr.Next()
}
}
func (s *weightedRoundRobinSelector) next() *Weighted {
if len(s.servers) == 0 {
return nil
}
n := len(s.servers)
if n == 0 {
return nil
}
if n == 1 {
return s.servers[0]
}
flag := 0
m := 0
for i := 0; i < n; i++ {
s.servers[i].CurrentWeight += s.servers[i].Weight
if s.servers[i].CurrentWeight > m {
m = s.servers[i].CurrentWeight
flag = i
}
}
s.servers[flag].CurrentWeight -= s.totalWeight
return s.servers[flag]
}
func createWeighted(servers map[string]string) []*Weighted {
ss := make([]*Weighted, 0, len(servers))
for k, metadata := range servers {
w := &Weighted{Server: k, Weight: 1}
if v, err := url.ParseQuery(metadata); err == nil {
ww := v.Get("weight")
if ww != "" {
if weight, err := strconv.Atoi(ww); err == nil {
w.Weight = weight
}
}
}
ss = append(ss, w)
}
return ss
}
type geoServer struct {
Server string
Latitude float64
Longitude float64
}
// geoSelector selects servers based on location.
type geoSelector struct {
servers []*geoServer
Latitude float64
Longitude float64
r *rand.Rand
}
func newGeoSelector(servers map[string]string, latitude, longitude float64) Selector {
ss := createGeoServer(servers)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return &geoSelector{servers: ss, Latitude: latitude, Longitude: longitude, r: r}
}
func (s *geoSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
if len(s.servers) == 0 {
return ""
}
var server []string
minNum := math.MaxFloat64
for _, gs := range s.servers {
d := getDistanceFrom(s.Latitude, s.Longitude, gs.Latitude, gs.Longitude)
if d < minNum {
server = []string{gs.Server}
minNum = d
} else if d == minNum {
server = append(server, gs.Server)
}
}
if len(server) == 1 {
return server[0]
}
return server[s.r.Intn(len(server))]
}
func (s *geoSelector) UpdateServer(servers map[string]string) {
ss := createGeoServer(servers)
s.servers = ss
}
func createGeoServer(servers map[string]string) []*geoServer {
geoServers := make([]*geoServer, 0, len(servers))
for s, metadata := range servers {
if v, err := url.ParseQuery(metadata); err == nil {
latStr := v.Get("latitude")
lonStr := v.Get("longitude")
if latStr == "" || lonStr == "" {
continue
}
lat, err := strconv.ParseFloat(latStr, 64)
if err != nil {
continue
}
lon, err := strconv.ParseFloat(lonStr, 64)
if err != nil {
continue
}
geoServers = append(geoServers, &geoServer{Server: s, Latitude: lat, Longitude: lon})
}
}
return geoServers
}
// consistentHashSelector selects based on JumpConsistentHash.
type consistentHashSelector struct {
h *doublejump.Hash
servers []string
}
func newConsistentHashSelector(servers map[string]string) Selector {
h := doublejump.NewHash()
ss := make([]string, 0, len(servers))
for k := range servers {
ss = append(ss, k)
h.Add(k)
}
sort.Slice(ss, func(i, j int) bool { return ss[i] < ss[j] })
return &consistentHashSelector{servers: ss, h: h}
}
func (s *consistentHashSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string {
ss := s.servers
if len(ss) == 0 {
return ""
}
key := genKey(servicePath, serviceMethod, args)
selected, _ := s.h.Get(key).(string)
return selected
}
func (s *consistentHashSelector) UpdateServer(servers map[string]string) {
ss := make([]string, 0, len(servers))
for k := range servers {
s.h.Add(k)
ss = append(ss, k)
}
sort.Slice(ss, func(i, j int) bool { return ss[i] < ss[j] })
for _, k := range s.servers {
if _, exist := servers[k]; !exist { // remove
s.h.Remove(k)
}
}
s.servers = ss
}
================================================
FILE: client/selector_test.go
================================================
package client
import (
"context"
"testing"
)
func Test_consistentHashSelector_Select(t *testing.T) {
servers := map[string]string{
"tcp@192.168.1.16:9392": "",
"tcp@192.168.1.16:9393": "",
}
s := newConsistentHashSelector(servers).(*consistentHashSelector)
key := uint64(9280147620691907957)
selected, _ := s.h.Get(key).(string)
for i := 0; i < 10000; i++ {
selected2, _ := s.h.Get(key).(string)
if selected != selected2 {
t.Errorf("expected %s but got %s", selected, selected2)
}
}
}
func Test_consistentHashSelector_UpdateServer(t *testing.T) {
servers := map[string]string{
"tcp@192.168.1.16:9392": "",
"tcp@192.168.1.16:9393": "",
}
s := newConsistentHashSelector(servers).(*consistentHashSelector)
if len(s.h.All()) != len(servers) {
t.Errorf("NewSelector: expected %d server but got %d", len(servers), len(s.h.All()))
}
s.UpdateServer(servers)
if len(s.h.All()) != len(servers) {
t.Errorf("UpdateServer: expected %d server but got %d", len(servers), len(s.h.All()))
}
}
func TestWeightedRoundRobinSelector_Select(t *testing.T) {
calc := make(map[string]int)
servers := make(map[string]string)
servers["ServerA"] = "weight=4"
servers["ServerB"] = "weight=2"
servers["ServerC"] = "weight=1"
weightSelector := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector)
ctx := context.Background()
for i := 0; i < 7; i++ {
s := weightSelector.Select(ctx, "", "", nil)
if _, ok := calc[s]; ok {
calc[s]++
} else {
calc[s] = 1
}
}
if calc["ServerA"] != 4 {
t.Errorf("expected %d but got %d", 4, calc["ServerA"])
}
if calc["ServerB"] != 2 {
t.Errorf("expected %d but got %d", 2, calc["ServerB"])
}
if calc["ServerC"] != 1 {
t.Errorf("expected %d but got %d", 1, calc["ServerC"])
}
}
func TestWeightedRoundRobinSelector_UpdateServer(t *testing.T) {
calc := make(map[string]int)
servers := make(map[string]string)
servers["ServerA"] = "weight=4"
servers["ServerB"] = "weight=2"
servers["ServerC"] = "weight=1"
weightSelector := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector)
ctx := context.Background()
servers["ServerA"] = "weight=5"
weightSelector.UpdateServer(servers)
for i := 0; i < 8; i++ {
s := weightSelector.Select(ctx, "", "", nil)
if _, ok := calc[s]; ok {
calc[s]++
} else {
calc[s] = 1
}
}
if calc["ServerA"] != 5 {
t.Errorf("expected %d but got %d", 4, calc["ServerA"])
}
if calc["ServerB"] != 2 {
t.Errorf("expected %d but got %d", 2, calc["ServerB"])
}
if calc["ServerC"] != 1 {
t.Errorf("expected %d but got %d", 1, calc["ServerC"])
}
}
func BenchmarkWeightedRoundRobinSelector_Select(b *testing.B) {
servers := make(map[string]string)
servers["ServerA"] = "weight=4"
servers["ServerB"] = "weight=2"
servers["ServerC"] = "weight=1"
ctx := context.Background()
weightSelector := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector)
for i := 0; i < b.N; i++ {
weightSelector.Select(ctx, "", "", nil)
}
}
//
//func TestWeightedICMPSelector(t *testing.T) {
// calc := make(map[string]int)
// servers := make(map[string]string)
// servers["@localhost:3333"] = ""
// servers["@www.baidu.com:3334"] = ""
// servers["@xxxx.xxxx:333"] = ""
// s := newWeightedICMPSelector(servers)
// ctx := context.Background()
// for i := 0; i < 10; i++ {
// host := s.Select(ctx, "", "", nil)
// if _, ok := calc[host]; ok {
// calc[host]++
// } else {
// calc[host] = 0
// }
// }
// if len(calc) != 2 {
// t.Errorf("expected %d but got %d", 2, len(servers))
// }
//}
//func TestWeightedICMPSelector_UpdateServer(t *testing.T) {
// calc := make(map[string]int)
// servers := make(map[string]string)
// servers["@localhost:3333"] = ""
// servers["@www.baidu.com:3334"] = ""
// servers["@xxxx.xxxx:333"] = ""
// s := newWeightedICMPSelector(servers)
// ctx := context.Background()
// servers["@www.sina.com:3333"] = ""
// s.UpdateServer(servers)
// for i := 0; i < 10; i++ {
// host := s.Select(ctx, "", "", nil)
// if _, ok := calc[host]; ok {
// calc[host]++
// } else {
// calc[host] = 0
// }
// }
// if len(calc) != 3 {
// t.Errorf("expected %d but got %d", 3, len(servers))
// }
//}
================================================
FILE: client/smooth_weighted_round_robin.go
================================================
package client
// Weighted is a wrapped server with weight
type Weighted struct {
Server string
Weight int
CurrentWeight int
EffectiveWeight int
}
================================================
FILE: client/xclient.go
================================================
package client
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"reflect"
"sort"
"strings"
"sync"
"time"
"github.com/juju/ratelimit"
"golang.org/x/sync/singleflight"
ex "github.com/smallnest/rpcx/errors"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
)
const (
FileTransferBufferSize = 1024
)
var (
// ErrXClientShutdown xclient is shutdown.
ErrXClientShutdown = errors.New("xClient is shut down")
// ErrXClientNoServer selector can't found one server.
ErrXClientNoServer = errors.New("can not found any server")
// ErrServerUnavailable selected server is unavailable.
ErrServerUnavailable = errors.New("selected server is unavailable")
)
// Receipt represents the result of the service returned.
type Receipt struct {
Address string
Reply interface{}
Error error
}
// XClient is an interface that used by client with service discovery and service governance.
// One XClient is used only for one service. You should create multiple XClient for multiple services.
type XClient interface {
SetPlugins(plugins PluginContainer)
GetPlugins() PluginContainer
SetSelector(s Selector)
ConfigGeoSelector(latitude, longitude float64)
Auth(auth string)
Go(ctx context.Context, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error)
Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error
Oneshot(ctx context.Context, serviceMethod string, args interface{}) error
Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error
Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error
Inform(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) ([]Receipt, error)
SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error)
SendFile(ctx context.Context, fileName string, rateInBytesPerSecond int64, meta map[string]string) error
DownloadFile(ctx context.Context, requestFileName string, saveTo io.Writer, meta map[string]string) error
Stream(ctx context.Context, meta map[string]string) (net.Conn, error)
Close() error
}
// SetSelector sets customized selector by users.
func (c *xClient) SetSelector(s Selector) {
c.mu.RLock()
s.UpdateServer(c.servers)
c.mu.RUnlock()
c.selector = s
}
// KVPair contains a key and a string.
type KVPair struct {
Key string
Value string
}
type xClient struct {
failMode FailMode
selectMode SelectMode
cachedClient map[string]RPCClient
breakers sync.Map
servicePath string
option Option
mu sync.RWMutex
servers map[string]string
unstableServers map[string]time.Time // 一些服务器重启,如果和它们建立链接,可能会耗费非常长的时间,这里记录袭来需要临时屏蔽
discovery ServiceDiscovery
selector Selector
stickyRPCClient RPCClient
stickyK string
slGroup singleflight.Group
isShutdown bool
// auth is a string for Authentication, for example, "Bearer mF_9.B5f-4.1JqM"
auth string
Plugins PluginContainer
ch chan []*KVPair
serverMessageChan chan<- *protocol.Message
}
// NewXClient creates a XClient that supports service discovery and service governance.
func NewXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) XClient {
client := &xClient{
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
servicePath: servicePath,
cachedClient: make(map[string]RPCClient),
unstableServers: make(map[string]time.Time),
option: option,
}
pairs := discovery.GetServices()
sort.Slice(pairs, func(i, j int) bool {
return strings.Compare(pairs[i].Key, pairs[j].Key) <= 0
})
servers := make(map[string]string, len(pairs))
for _, p := range pairs {
servers[p.Key] = p.Value
}
filterByStateAndGroup(client.option.Group, servers)
client.servers = servers
if selectMode != Closest && selectMode != SelectByUser {
client.selector = newSelector(selectMode, servers)
}
client.Plugins = &pluginContainer{}
ch := client.discovery.WatchService()
if ch != nil {
client.ch = ch
go client.watch(ch)
}
return client
}
// NewBidirectionalXClient creates a new xclient that can receive notifications from servers.
func NewBidirectionalXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) XClient {
client := &xClient{
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
servicePath: servicePath,
cachedClient: make(map[string]RPCClient),
option: option,
unstableServers: make(map[string]time.Time),
serverMessageChan: serverMessageChan,
}
pairs := discovery.GetServices()
sort.Slice(pairs, func(i, j int) bool {
return strings.Compare(pairs[i].Key, pairs[j].Key) <= 0
})
servers := make(map[string]string, len(pairs))
for _, p := range pairs {
servers[p.Key] = p.Value
}
filterByStateAndGroup(client.option.Group, servers)
client.servers = servers
if selectMode != Closest && selectMode != SelectByUser {
client.selector = newSelector(selectMode, servers)
}
client.Plugins = &pluginContainer{}
ch := client.discovery.WatchService()
if ch != nil {
client.ch = ch
go client.watch(ch)
}
return client
}
// SetPlugins sets client's plugins.
func (c *xClient) SetPlugins(plugins PluginContainer) {
c.Plugins = plugins
}
func (c *xClient) GetPlugins() PluginContainer {
return c.Plugins
}
// ConfigGeoSelector sets location of client's latitude and longitude,
// and use newGeoSelector.
func (c *xClient) ConfigGeoSelector(latitude, longitude float64) {
c.selector = newGeoSelector(c.servers, latitude, longitude)
c.selectMode = Closest
}
// Auth sets s token for Authentication.
func (c *xClient) Auth(auth string) {
c.auth = auth
}
// watch changes of service and update cached clients.
func (c *xClient) watch(ch chan []*KVPair) {
for pairs := range ch {
sort.Slice(pairs, func(i, j int) bool {
return strings.Compare(pairs[i].Key, pairs[j].Key) <= 0
})
servers := make(map[string]string, len(pairs))
for _, p := range pairs {
servers[p.Key] = p.Value
}
c.mu.Lock()
filterByStateAndGroup(c.option.Group, servers)
c.servers = servers
if c.selector != nil {
c.selector.UpdateServer(servers)
}
c.mu.Unlock()
}
}
func filterByStateAndGroup(group string, servers map[string]string) {
for k, v := range servers {
if values, err := url.ParseQuery(v); err == nil {
if state := values.Get("state"); state == "inactive" {
delete(servers, k)
}
groups := values["group"] // Directly access the map to get all values associated with "group" as a slice
if group != "" {
found := false
for _, g := range groups {
if group == g {
found = true
break // A matching group is found, stop the search
}
}
if !found {
delete(servers, k) // If no matching group is found, delete the corresponding server from the map
}
}
}
}
}
// selects a client from candidates base on c.selectMode
func (c *xClient) selectClient(ctx context.Context, servicePath, serviceMethod string, args interface{}) (string, RPCClient, error) {
c.mu.Lock()
if c.option.Sticky && c.stickyRPCClient != nil {
if c.stickyRPCClient.IsClosing() || c.stickyRPCClient.IsShutdown() {
c.stickyRPCClient = nil
}
}
if c.option.Sticky && c.stickyRPCClient != nil {
c.mu.Unlock()
return c.stickyK, c.stickyRPCClient, nil
}
fn := c.selector.Select
if c.Plugins != nil {
fn = c.Plugins.DoWrapSelect(fn)
}
k := fn(ctx, servicePath, serviceMethod, args)
c.mu.Unlock()
if k == "" {
return "", nil, ErrXClientNoServer
}
client, err := c.getCachedClient(k, servicePath, serviceMethod, args)
if c.option.Sticky && client != nil {
c.mu.Lock()
safeCloseClient(c.stickyRPCClient)
c.stickyK = k
c.stickyRPCClient = client
c.mu.Unlock()
}
return k, client, err
}
func safeCloseClient(client RPCClient) {
if client == nil {
return
}
defer func() {
_ = recover()
}()
client.Close()
}
func (c *xClient) getCachedClient(k string, servicePath, serviceMethod string, _ interface{}) (client RPCClient, err error) {
var needCallPlugin bool
defer func() {
if needCallPlugin {
_, err = c.Plugins.DoClientConnected(client.GetConn())
}
}()
if c.isShutdown {
return nil, errors.New("this xclient is closed")
}
// if this client is broken
breaker, ok := c.breakers.Load(k)
if ok && !breaker.(Breaker).Ready() {
return nil, ErrBreakerOpen
}
c.mu.Lock()
defer c.mu.Unlock()
client = c.findCachedClient(k, servicePath, serviceMethod)
if client != nil {
if !client.IsClosing() && !client.IsShutdown() {
return client, nil
}
c.deleteCachedClient(client, k, servicePath, serviceMethod)
}
client = c.findCachedClient(k, servicePath, serviceMethod)
if client == nil || client.IsShutdown() {
generatedClient, err, _ := c.slGroup.Do(k, func() (interface{}, error) {
return c.generateClient(k, servicePath, serviceMethod)
})
if err != nil {
c.slGroup.Forget(k)
return nil, err
}
client = generatedClient.(RPCClient)
if c.Plugins != nil {
needCallPlugin = true
}
client.RegisterServerMessageChan(c.serverMessageChan)
c.setCachedClient(client, k, servicePath, serviceMethod)
// forget k only when client is cached
c.slGroup.Forget(k)
}
return client, nil
}
func (c *xClient) setCachedClient(client RPCClient, k, servicePath, serviceMethod string) {
network, _ := splitNetworkAndAddress(k)
if builder, ok := getCacheClientBuilder(network); ok {
builder.SetCachedClient(client, k, servicePath, serviceMethod)
return
}
c.cachedClient[k] = client
}
func (c *xClient) findCachedClient(k, servicePath, serviceMethod string) RPCClient {
network, _ := splitNetworkAndAddress(k)
if builder, ok := getCacheClientBuilder(network); ok {
return builder.FindCachedClient(k, servicePath, serviceMethod)
}
return c.cachedClient[k]
}
func (c *xClient) deleteCachedClient(client RPCClient, k, servicePath, serviceMethod string) {
network, _ := splitNetworkAndAddress(k)
if builder, ok := getCacheClientBuilder(network); ok && client != nil {
builder.DeleteCachedClient(client, k, servicePath, serviceMethod)
client.Close()
return
}
delete(c.cachedClient, k)
if client != nil {
client.Close()
}
}
func (c *xClient) removeClient(k, servicePath, serviceMethod string, client RPCClient) {
c.mu.Lock()
if c.option.Sticky {
c.stickyK = ""
c.stickyRPCClient = nil
}
cl := c.findCachedClient(k, servicePath, serviceMethod)
if cl == client {
c.deleteCachedClient(client, k, servicePath, serviceMethod)
}
c.mu.Unlock()
if client != nil {
client.UnregisterServerMessageChan()
client.Close()
}
}
func (c *xClient) generateClient(k, servicePath, serviceMethod string) (client RPCClient, err error) {
network, addr := splitNetworkAndAddress(k)
if builder, ok := getCacheClientBuilder(network); ok && builder != nil {
return builder.GenerateClient(k, servicePath, serviceMethod)
}
client = &Client{
option: c.option,
Plugins: c.Plugins,
}
var breaker interface{}
if c.option.GenBreaker != nil {
breaker, _ = c.breakers.LoadOrStore(k, c.option.GenBreaker())
}
err = client.Connect(network, addr)
if err != nil {
if breaker != nil {
breaker.(Breaker).Fail()
}
return nil, err
}
return client, err
}
func (c *xClient) getCachedClientWithoutLock(k, servicePath, serviceMethod string) (RPCClient, bool, error) {
var needCallPlugin bool
client := c.findCachedClient(k, servicePath, serviceMethod)
if client != nil {
if !client.IsClosing() && !client.IsShutdown() {
return client, needCallPlugin, nil
}
c.deleteCachedClient(client, k, servicePath, serviceMethod)
// double check
client = c.findCachedClient(k, servicePath, serviceMethod)
}
if client == nil || client.IsShutdown() {
generatedClient, err, _ := c.slGroup.Do(k, func() (interface{}, error) {
return c.generateClient(k, servicePath, serviceMethod)
})
if err != nil {
c.slGroup.Forget(k)
return nil, needCallPlugin, err
}
client = generatedClient.(RPCClient)
if c.Plugins != nil {
needCallPlugin = true
}
client.RegisterServerMessageChan(c.serverMessageChan)
c.setCachedClient(client, k, servicePath, serviceMethod)
c.slGroup.Forget(k)
}
return client, needCallPlugin, nil
}
func splitNetworkAndAddress(server string) (string, string) {
ss := strings.SplitN(server, "@", 2)
if len(ss) == 1 {
return "tcp", server
}
return ss[0], ss[1]
}
func setServerTimeout(ctx context.Context) context.Context {
if deadline, ok := ctx.Deadline(); ok {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.ServerTimeout] = fmt.Sprintf("%d", time.Until(deadline).Milliseconds())
}
return ctx
}
// Go invokes the function asynchronously. It returns the Call structure representing the invocation. The done channel will signal when the call is complete by returning the same Call object. If done is nil, Go will allocate a new channel. If non-nil, done must be buffered or Go will deliberately crash.
// It does not use FailMode.
func (c *xClient) Go(ctx context.Context, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error) {
if c.isShutdown {
return nil, ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
ctx = setServerTimeout(ctx)
if share.Trace {
log.Debugf("select a client for %s.%s, args: %+v in case of xclient Go", c.servicePath, serviceMethod, args)
}
_, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args)
if err != nil {
return nil, err
}
if share.Trace {
log.Debugf("selected a client %s for %s.%s, args: %+v in case of xclient Go", client.RemoteAddr(), c.servicePath, serviceMethod, args)
}
if done == nil {
done = make(chan *Call, 10)
}
return client.Go(ctx, c.servicePath, serviceMethod, args, reply, done), nil
}
// Call invokes the named function, waits for it to complete, and returns its error status.
// It handles errors base on FailMode.
func (c *xClient) Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error {
if c.isShutdown {
return ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
ctx = setServerTimeout(ctx)
if share.Trace {
log.Debugf("select a client for %s.%s, failMode: %v, args: %+v in case of xclient Call", c.servicePath, serviceMethod, c.failMode, args)
}
var err error
k, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args)
if err != nil {
if c.failMode == Failfast || contextCanceled(err) {
return err
}
}
if share.Trace {
if client != nil {
log.Debugf("selected a client %s for %s.%s, failMode: %v, args: %+v in case of xclient Call", client.RemoteAddr(), c.servicePath, serviceMethod, c.failMode, args)
} else {
log.Debugf("selected a client %s for %s.%s, failMode: %v, args: %+v in case of xclient Call", "nil", c.servicePath, serviceMethod, c.failMode, args)
}
}
var e error
switch c.failMode {
case Failtry:
retries := c.option.Retries
retryInterval := c.option.RetryInterval
for retries >= 0 {
retries--
if client != nil {
err = c.wrapCall(ctx, client, serviceMethod, args, reply)
if err == nil {
return nil
}
if contextCanceled(err) {
return err
}
if e, ok := err.(ServiceError); ok && e.IsServiceError() {
return err
}
}
if uncoverError(err) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
client, e = c.getCachedClient(k, c.servicePath, serviceMethod, args)
time.Sleep(retryInterval)
}
if err == nil {
err = e
}
return err
case Failover:
retries := c.option.Retries
retryInterval := c.option.RetryInterval
for retries >= 0 {
retries--
if client != nil {
err = c.wrapCall(ctx, client, serviceMethod, args, reply)
if err == nil {
return nil
}
if contextCanceled(err) {
return err
}
if e, ok := err.(ServiceError); ok && e.IsServiceError() {
return err
}
}
if uncoverError(err) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
time.Sleep(retryInterval)
// select another server
k, client, e = c.selectClient(ctx, c.servicePath, serviceMethod, args)
}
if err == nil {
err = e
}
return err
case Failbackup:
ctx, cancelFn := context.WithCancel(ctx)
defer cancelFn()
call1 := make(chan *Call, 10)
call2 := make(chan *Call, 10)
var reply1, reply2 interface{}
if reply != nil {
reply1 = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
reply2 = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
}
_, err1 := c.Go(ctx, serviceMethod, args, reply1, call1)
t := time.NewTimer(c.option.BackupLatency)
select {
case <-ctx.Done(): // cancel by context
err = ctx.Err()
return err
case call := <-call1:
err = call.Error
if err == nil && reply != nil {
reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply1).Elem())
}
return err
case <-t.C:
}
_, err2 := c.Go(ctx, serviceMethod, args, reply2, call2)
if err2 != nil {
if uncoverError(err2) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
err = err1
return err
}
select {
case <-ctx.Done(): // cancel by context
err = ctx.Err()
case call := <-call1:
err = call.Error
if err == nil && reply != nil && reply1 != nil {
reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply1).Elem())
}
case call := <-call2:
err = call.Error
if err == nil && reply != nil && reply2 != nil {
reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply2).Elem())
}
}
return err
default: // Failfast
err = c.wrapCall(ctx, client, serviceMethod, args, reply)
if err != nil {
if uncoverError(err) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
}
return err
}
}
// Oneshot invokes the named function, ** DOEST NOT ** wait for it to complete, and returns immediately.
func (c *xClient) Oneshot(ctx context.Context, serviceMethod string, args interface{}) error {
if c.isShutdown {
return ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
ctx = setServerTimeout(ctx)
if share.Trace {
log.Debugf("select a client for %s.%s, args: %+v in case of xclient Go", c.servicePath, serviceMethod, args)
}
_, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args)
if err != nil {
return err
}
if share.Trace {
log.Debugf("selected a client %s for %s.%s, args: %+v in case of xclient Go", client.RemoteAddr(), c.servicePath, serviceMethod, args)
}
client.Go(ctx, c.servicePath, serviceMethod, args, nil, nil)
return nil
}
func uncoverError(err error) bool {
if e, ok := err.(ServiceError); ok && e.IsServiceError() {
return false
}
if err == context.DeadlineExceeded {
return false
}
if err == context.Canceled {
return false
}
return true
}
func contextCanceled(err error) bool {
if err == context.DeadlineExceeded {
return true
}
if err == context.Canceled {
return true
}
return false
}
func (c *xClient) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) {
if c.isShutdown {
return nil, nil, ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
ctx = setServerTimeout(ctx)
if share.Trace {
log.Debugf("select a client for %s.%s, failMode: %v, args: %+v in case of xclient SendRaw", r.ServicePath, r.ServiceMethod, c.failMode, r.Payload)
}
var err error
k, client, err := c.selectClient(ctx, r.ServicePath, r.ServiceMethod, r.Payload)
if err != nil {
if c.failMode == Failfast {
return nil, nil, err
}
if contextCanceled(err) {
return nil, nil, err
}
if e, ok := err.(ServiceError); ok && e.IsServiceError() {
return nil, nil, err
}
}
if share.Trace {
log.Debugf("selected a client %s for %s.%s, failMode: %v, args: %+v in case of xclient Call", client.RemoteAddr(), r.ServicePath, r.ServiceMethod, c.failMode, r.Payload)
}
var e error
switch c.failMode {
case Failtry:
retries := c.option.Retries
for retries >= 0 {
retries--
if client != nil {
m, payload, err := c.wrapSendRaw(ctx, client, r)
if err == nil {
return m, payload, nil
}
if contextCanceled(err) {
return nil, nil, err
}
if _, ok := err.(ServiceError); ok {
return nil, nil, err
}
}
if uncoverError(err) {
c.removeClient(k, r.ServicePath, r.ServiceMethod, client)
}
client, e = c.getCachedClient(k, r.ServicePath, r.ServiceMethod, r.Payload)
}
if err == nil {
err = e
}
return nil, nil, err
case Failover:
retries := c.option.Retries
for retries >= 0 {
retries--
if client != nil {
m, payload, err := c.wrapSendRaw(ctx, client, r)
if err == nil {
return m, payload, nil
}
if contextCanceled(err) {
return nil, nil, err
}
if e, ok := err.(ServiceError); ok && e.IsServiceError() {
return nil, nil, err
}
}
if uncoverError(err) {
c.removeClient(k, r.ServicePath, r.ServiceMethod, client)
}
// select another server
k, client, e = c.selectClient(ctx, r.ServicePath, r.ServiceMethod, r.Payload)
}
if err == nil {
err = e
}
return nil, nil, err
default: // Failfast
m, payload, err := c.wrapSendRaw(ctx, client, r)
if err != nil {
if uncoverError(err) {
c.removeClient(k, r.ServicePath, r.ServiceMethod, client)
}
}
return m, payload, nil
}
}
func (c *xClient) wrapCall(ctx context.Context, client RPCClient, serviceMethod string, args interface{}, reply interface{}) error {
if client == nil {
return ErrServerUnavailable
}
if share.Trace {
log.Debugf("call a client for %s.%s, args: %+v in case of xclient wrapCall", c.servicePath, serviceMethod, args)
}
if _, ok := ctx.(*share.Context); !ok {
ctx = share.NewContext(ctx)
}
err := c.Plugins.DoPreCall(ctx, c.servicePath, serviceMethod, args)
if err != nil {
return err
}
err = client.Call(ctx, c.servicePath, serviceMethod, args, reply)
c.Plugins.DoPostCall(ctx, c.servicePath, serviceMethod, args, reply, err)
if share.Trace {
log.Debugf("called a client for %s.%s, args: %+v, err: %v in case of xclient wrapCall", c.servicePath, serviceMethod, args, err)
}
return err
}
// wrapSendRaw wrap SendRaw to support client plugins
func (c *xClient) wrapSendRaw(ctx context.Context, client RPCClient, r *protocol.Message) (map[string]string, []byte, error) {
if client == nil {
return nil, nil, ErrServerUnavailable
}
if share.Trace {
log.Debugf("call a client for %s.%s, args: %+v in case of xclient wrapSendRaw", c.servicePath, r.ServiceMethod, r.Payload)
}
ctx = share.NewContext(ctx)
err := c.Plugins.DoPreCall(ctx, c.servicePath, r.ServiceMethod, r.Payload)
if err != nil {
return nil, nil, err
}
m, payload, err := client.SendRaw(ctx, r)
c.Plugins.DoPostCall(ctx, c.servicePath, r.ServiceMethod, r.Payload, nil, err)
if share.Trace {
log.Debugf("called a client for %s.%s, args: %+v, err: %v in case of xclient wrapSendRaw", c.servicePath, r.ServiceMethod, r.Payload, err)
}
return m, payload, err
}
// Broadcast sends requests to all servers and Success only when all servers return OK.
// FailMode and SelectMode are meanless for this method.
// Please set timeout to avoid hanging.
func (c *xClient) Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error {
if c.isShutdown {
return ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
var replyOnce sync.Once
ctx = setServerTimeout(ctx)
// add timeout after set server timeout, only prevent client hanging
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
callPlugins := make([]RPCClient, 0, len(c.servers))
clients := make(map[string]RPCClient)
c.mu.Lock()
for k := range c.servers {
client, needCallPlugin, err := c.getCachedClientWithoutLock(k, c.servicePath, serviceMethod)
if err != nil {
continue
}
clients[k] = client
if needCallPlugin {
callPlugins = append(callPlugins, client)
}
}
c.mu.Unlock()
for i := range callPlugins {
if c.Plugins != nil {
c.Plugins.DoClientConnected(callPlugins[i].GetConn())
}
}
if len(clients) == 0 {
return ErrXClientNoServer
}
err := &ex.MultiError{}
l := len(clients)
done := make(chan bool, l)
for k, client := range clients {
k := k
client := client
go func() {
var clonedReply interface{}
if reply != nil {
clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
}
e := c.wrapCall(ctx, client, serviceMethod, args, clonedReply)
defer func() {
done <- (e == nil)
}()
if e != nil {
if uncoverError(e) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
err.Append(e)
}
if e == nil && reply != nil && clonedReply != nil {
replyOnce.Do(func() {
reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem())
})
}
}()
}
check:
for {
select {
case result := <-done:
l--
if l == 0 || !result { // all returns or some one returns an error
break check
}
}
}
select {
case <-ctx.Done():
err.Append(errors.New(("timeout")))
default:
}
return err.ErrorOrNil()
}
// Fork sends requests to all servers and Success once one server returns OK.
// FailMode and SelectMode are meanless for this method.
func (c *xClient) Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error {
if c.isShutdown {
return ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
ctx = setServerTimeout(ctx)
// add timeout after set server timeout, only prevent client hanging
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
callPlugins := make([]RPCClient, 0, len(c.servers))
clients := make(map[string]RPCClient)
c.mu.Lock()
for k := range c.servers {
client, needCallPlugin, err := c.getCachedClientWithoutLock(k, c.servicePath, serviceMethod)
if err != nil {
continue
}
clients[k] = client
if needCallPlugin {
callPlugins = append(callPlugins, client)
}
}
c.mu.Unlock()
for i := range callPlugins {
if c.Plugins != nil {
c.Plugins.DoClientConnected(callPlugins[i].GetConn())
}
}
if len(clients) == 0 {
return ErrXClientNoServer
}
var replyOnce sync.Once
err := &ex.MultiError{}
l := len(clients)
done := make(chan bool, l)
for k, client := range clients {
k := k
client := client
go func() {
var clonedReply interface{}
if reply != nil {
clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
}
e := c.wrapCall(ctx, client, serviceMethod, args, clonedReply)
if e == nil && reply != nil && clonedReply != nil {
replyOnce.Do(func() {
reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem())
})
}
defer func() {
done <- (e == nil)
}()
if e != nil {
if uncoverError(e) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
err.Append(e)
}
}()
}
check:
for {
select {
case result := <-done:
l--
if result {
return nil
}
if l == 0 { // all returns or some one returns an error
break check
}
}
}
select {
case <-ctx.Done():
err.Append(errors.New(("timeout")))
default:
}
return err.ErrorOrNil()
}
// Inform sends requests to all servers and returns all results from services.
// FailMode and SelectMode are meanless for this method.
// Please set timeout to avoid hanging.
func (c *xClient) Inform(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) ([]Receipt, error) {
if c.isShutdown {
return nil, ErrXClientShutdown
}
if c.auth != "" {
metadata := ctx.Value(share.ReqMetaDataKey)
if metadata == nil {
metadata = map[string]string{}
ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata)
}
m := metadata.(map[string]string)
m[share.AuthKey] = c.auth
}
ctx = setServerTimeout(ctx)
// add timeout after set server timeout, only prevent client hanging
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
callPlugins := make([]RPCClient, 0, len(c.servers))
clients := make(map[string]RPCClient)
c.mu.Lock()
for k := range c.servers {
client, needCallPlugin, err := c.getCachedClientWithoutLock(k, c.servicePath, serviceMethod)
if err != nil {
continue
}
clients[k] = client
if needCallPlugin {
callPlugins = append(callPlugins, client)
}
}
c.mu.Unlock()
for i := range callPlugins {
if c.Plugins != nil {
c.Plugins.DoClientConnected(callPlugins[i].GetConn())
}
}
if len(clients) == 0 {
return nil, ErrXClientNoServer
}
var receiptsLock sync.Mutex
var receipts []Receipt
var replyOnce sync.Once
err := &ex.MultiError{}
l := len(clients)
done := make(chan bool, l)
for k, client := range clients {
k := k
client := client
go func() {
var clonedReply interface{}
if reply != nil {
clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface()
}
e := c.wrapCall(ctx, client, serviceMethod, args, clonedReply)
defer func() {
done <- (e == nil)
}()
if e != nil {
if uncoverError(e) {
c.removeClient(k, c.servicePath, serviceMethod, client)
}
err.Append(e)
}
if e == nil && reply != nil && clonedReply != nil {
replyOnce.Do(func() {
reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem())
})
}
addr := k
ss := strings.SplitN(k, "@", 2)
if len(ss) == 2 {
addr = ss[1]
}
receiptsLock.Lock()
receipts = append(receipts, Receipt{
Address: addr,
Reply: clonedReply,
Error: err,
})
receiptsLock.Unlock()
}()
}
check:
for {
select {
case <-done:
l--
if l == 0 { // all returns or some one returns an error
break check
}
}
}
select {
case <-ctx.Done():
err.Append(errors.New(("timeout")))
default:
}
return receipts, err.ErrorOrNil()
}
// SendFile sends a local file to the server.
// fileName is the path of local file.
// rateInBytesPerSecond can limit bandwidth of sending, 0 means does not limit the bandwidth, unit is bytes / second.
func (c *xClient) SendFile(ctx context.Context, fileName string, rateInBytesPerSecond int64, meta map[string]string) error {
file, err := os.Open(fileName)
if err != nil {
return err
}
defer file.Close()
fi, err := os.Stat(fileName)
if err != nil {
return err
}
args := share.FileTransferArgs{
FileName: fi.Name(),
FileSize: fi.Size(),
Meta: meta,
}
ctx = setServerTimeout(ctx)
reply := &share.FileTransferReply{}
err = c.Call(ctx, "TransferFile", args, reply)
if err != nil {
return err
}
conn, err := net.DialTimeout("tcp", reply.Addr, c.option.ConnectTimeout)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write(reply.Token)
if err != nil {
return err
}
var tb *ratelimit.Bucket
if rateInBytesPerSecond > 0 {
tb = ratelimit.NewBucketWithRate(float64(rateInBytesPerSecond), rateInBytesPerSecond)
}
sendBuffer := make([]byte, FileTransferBufferSize)
loop:
for {
select {
case <-ctx.Done():
err = ctx.Err()
break loop
default:
if tb != nil {
tb.Wait(FileTransferBufferSize)
}
n, err := file.Read(sendBuffer)
if err != nil {
if err == io.EOF {
return nil
} else {
return err
}
}
if n == 0 {
break loop
}
_, err = conn.Write(sendBuffer[:n])
if err != nil {
if err == io.EOF {
return nil
} else {
return err
}
}
}
}
return nil
}
func (c *xClient) DownloadFile(ctx context.Context, requestFileName string, saveTo io.Writer, meta map[string]string) error {
ctx = setServerTimeout(ctx)
args := share.DownloadFileArgs{
FileName: requestFileName,
Meta: meta,
}
reply := &share.FileTransferReply{}
err := c.Call(ctx, "DownloadFile", args, reply)
if err != nil {
return err
}
conn, err := net.DialTimeout("tcp", reply.Addr, c.option.ConnectTimeout)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write(reply.Token)
if err != nil {
return err
}
buf := make([]byte, FileTransferBufferSize)
r := bufio.NewReader(conn)
loop:
for {
select {
case <-ctx.Done():
err = ctx.Err()
break loop
default:
n, er := r.Read(buf)
if n > 0 {
_, ew := saveTo.Write(buf[0:n])
if ew != nil {
err = ew
break loop
}
}
if er != nil {
if er != io.EOF {
err = er
}
break loop
}
}
}
return err
}
// Close closes this client and its underlying connections to services.
func (c *xClient) Close() error {
var errs []error
c.mu.Lock()
c.isShutdown = true
for k, v := range c.cachedClient {
e := v.Close()
if e != nil {
errs = append(errs, e)
}
delete(c.cachedClient, k)
}
c.mu.Unlock()
go func() {
defer func() {
recover()
}()
c.discovery.RemoveWatcher(c.ch)
close(c.ch)
}()
if len(errs) > 0 {
return ex.NewMultiError(errs)
}
return nil
}
func (c *xClient) Stream(ctx context.Context, meta map[string]string) (net.Conn, error) {
args := share.StreamServiceArgs{
Meta: meta,
}
ctx = setServerTimeout(ctx)
reply := &share.StreamServiceReply{}
err := c.Call(ctx, "Stream", args, reply)
if err != nil {
return nil, err
}
conn, err := net.DialTimeout("tcp", reply.Addr, c.option.ConnectTimeout)
if err != nil {
return nil, err
}
_, err = conn.Write(reply.Token)
if err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
================================================
FILE: client/xclient_pool.go
================================================
package client
import (
"sync"
"sync/atomic"
"github.com/smallnest/rpcx/protocol"
)
// XClientPool is a xclient pool with fixed size.
// It uses roundrobin algorithm to call its xclients.
// All xclients share the same configurations such as ServiceDiscovery and serverMessageChan.
type XClientPool struct {
count uint64
index uint64
xclients []XClient
mu sync.RWMutex
servicePath string
failMode FailMode
selectMode SelectMode
discovery ServiceDiscovery
option Option
auth string
Plugins PluginContainer
serverMessageChan chan<- *protocol.Message
}
// NewXClientPool creates a fixed size XClient pool.
func NewXClientPool(count int, servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) *XClientPool {
pool := &XClientPool{
count: uint64(count),
xclients: make([]XClient, count),
servicePath: servicePath,
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
option: option,
}
for i := 0; i < count; i++ {
xclient := NewXClient(servicePath, failMode, selectMode, discovery, option)
pool.xclients[i] = xclient
}
return pool
}
// NewBidirectionalXClientPool creates a BidirectionalXClient pool with fixed size.
func NewBidirectionalXClientPool(count int, servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) *XClientPool {
pool := &XClientPool{
count: uint64(count),
xclients: make([]XClient, count),
servicePath: servicePath,
failMode: failMode,
selectMode: selectMode,
discovery: discovery,
option: option,
serverMessageChan: serverMessageChan,
}
for i := 0; i < count; i++ {
xclient := NewBidirectionalXClient(servicePath, failMode, selectMode, discovery, option, serverMessageChan)
pool.xclients[i] = xclient
}
return pool
}
// Auth sets s token for Authentication.
func (c *XClientPool) Auth(auth string) {
c.auth = auth
c.mu.RLock()
for _, v := range c.xclients {
v.Auth(auth)
}
c.mu.RUnlock()
}
// SetPlugins sets client's plugins.
func (p *XClientPool) SetPlugins(plugins PluginContainer) {
p.Plugins = plugins
p.mu.RLock()
for _, v := range p.xclients {
v.SetPlugins(plugins)
}
p.mu.RUnlock()
}
// GetPlugins returns client's plugins.
func (p *XClientPool) GetPlugins() PluginContainer {
return p.Plugins
}
// Get returns a xclient.
// It does not remove this xclient from its cache so you don't need to put it back.
// Don't close this xclient because maybe other goroutines are using this xclient.
func (p *XClientPool) Get() XClient {
i := atomic.AddUint64(&p.index, 1)
picked := int(i % p.count)
return p.xclients[picked]
}
// Close this pool.
// Please make sure it won't be used any more.
func (p *XClientPool) Close() {
for _, c := range p.xclients {
c.Close()
}
p.xclients = nil
}
================================================
FILE: client/xclient_pool_test.go
================================================
package client
import (
"testing"
)
// testPlugin is a simple test plugin implementation
type testPlugin struct {
name string
}
func TestXClientPool_SetPlugins(t *testing.T) {
// Create a simple discovery
pairs := []*KVPair{
{Key: "tcp@127.0.0.1:8972", Value: ""},
}
discovery, err := NewMultipleServersDiscovery(pairs)
if err != nil {
t.Fatalf("failed to create discovery: %v", err)
}
defer discovery.Close()
// Create a pool
pool := NewXClientPool(3, "Arith", Failtry, RandomSelect, discovery, DefaultOption)
defer pool.Close()
// Create plugins
plugins := NewPluginContainer()
tp := &testPlugin{name: "test-plugin"}
plugins.Add(tp)
// Test SetPlugins
pool.SetPlugins(plugins)
// Verify plugins are set on pool
if pool.GetPlugins() == nil {
t.Error("plugins should not be nil after SetPlugins")
}
if pool.GetPlugins() != plugins {
t.Error("pool plugins should be the same as the set plugins")
}
// Verify plugins are set on all xclients
for i := 0; i < 3; i++ {
xclient := pool.Get()
if xclient.GetPlugins() == nil {
t.Errorf("xclient %d plugins should not be nil", i)
}
if xclient.GetPlugins() != plugins {
t.Errorf("xclient %d plugins should be the same as the set plugins", i)
}
}
}
func TestXClientPool_GetPlugins(t *testing.T) {
// Create a simple discovery
pairs := []*KVPair{
{Key: "tcp@127.0.0.1:8972", Value: ""},
}
discovery, err := NewMultipleServersDiscovery(pairs)
if err != nil {
t.Fatalf("failed to create discovery: %v", err)
}
defer discovery.Close()
// Create a pool
pool := NewXClientPool(2, "Arith", Failtry, RandomSelect, discovery, DefaultOption)
defer pool.Close()
// Initially, plugins should be nil
if pool.GetPlugins() != nil {
t.Error("plugins should be nil initially")
}
// Create and set plugins
plugins := NewPluginContainer()
tp := &testPlugin{name: "test-plugin"}
plugins.Add(tp)
pool.SetPlugins(plugins)
// Verify GetPlugins returns the correct plugins
retrievedPlugins := pool.GetPlugins()
if retrievedPlugins == nil {
t.Error("plugins should not be nil after SetPlugins")
}
if retrievedPlugins != plugins {
t.Error("GetPlugins should return the same plugins as SetPlugins")
}
// Verify plugins contain the test plugin
allPlugins := retrievedPlugins.All()
if len(allPlugins) != 1 {
t.Errorf("expected 1 plugin, got %d", len(allPlugins))
}
if p, ok := allPlugins[0].(*testPlugin); !ok || p.name != "test-plugin" {
t.Error("plugin should be the test plugin")
}
}
func TestXClientPool_SetPlugins_Concurrent(t *testing.T) {
// Create a simple discovery
pairs := []*KVPair{
{Key: "tcp@127.0.0.1:8972", Value: ""},
}
discovery, err := NewMultipleServersDiscovery(pairs)
if err != nil {
t.Fatalf("failed to create discovery: %v", err)
}
defer discovery.Close()
// Create a pool
pool := NewXClientPool(5, "Arith", Failtry, RandomSelect, discovery, DefaultOption)
defer pool.Close()
// Test concurrent SetPlugins calls
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(id int) {
plugins := NewPluginContainer()
tp := &testPlugin{name: "test-plugin"}
plugins.Add(tp)
pool.SetPlugins(plugins)
done <- true
}(i)
}
// Wait for all goroutines to complete
for i := 0; i < 10; i++ {
<-done
}
// Verify plugins are set
if pool.GetPlugins() == nil {
t.Error("plugins should not be nil after concurrent SetPlugins")
}
}
================================================
FILE: client/xclient_test.go
================================================
package client
import (
"context"
"errors"
"testing"
"time"
"fmt"
testutils "github.com/smallnest/rpcx/_testutils"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/server"
"github.com/smallnest/rpcx/share"
)
func TestXClient_Thrift(t *testing.T) {
s := server.NewServer()
s.RegisterName("Arith", new(Arith), "")
go s.Serve("tcp", "127.0.0.1:0")
defer s.Close()
time.Sleep(500 * time.Millisecond)
addr := s.Address().String()
opt := Option{
Retries: 1,
RPCPath: share.DefaultRPCPath,
ConnectTimeout: 10 * time.Second,
SerializeType: protocol.Thrift,
CompressType: protocol.None,
BackupLatency: 10 * time.Millisecond,
}
d, err := NewPeer2PeerDiscovery("tcp@"+addr, "desc=a test service")
if err != nil {
t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err)
}
xclient := NewXClient("Arith", Failtry, RandomSelect, d, opt)
defer xclient.Close()
args := testutils.ThriftArgs_{}
args.A = 200
args.B = 100
reply := testutils.ThriftReply{}
err = xclient.Call(context.Background(), "ThriftMul", &args, &reply)
if err != nil {
t.Fatalf("failed to call: %v", err)
}
fmt.Println(reply.C)
if reply.C != 20000 {
t.Fatalf("expect 20000 but got %d", reply.C)
}
}
func TestXClient_IT(t *testing.T) {
s := server.NewServer()
s.RegisterName("Arith", new(Arith), "")
go s.Serve("tcp", "127.0.0.1:0")
defer s.Close()
time.Sleep(500 * time.Millisecond)
addr := s.Address().String()
d, err := NewPeer2PeerDiscovery("tcp@"+addr, "desc=a test service")
if err != nil {
t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err)
}
xclient := NewXClient("Arith", Failtry, RandomSelect, d, DefaultOption)
defer xclient.Close()
args := &Args{
A: 10,
B: 20,
}
reply := &Reply{}
err = xclient.Call(context.Background(), "Mul", args, reply)
if err != nil {
t.Fatalf("failed to call: %v", err)
}
if reply.C != 200 {
t.Fatalf("expect 200 but got %d", reply.C)
}
}
func TestXClient_filterByStateAndGroup(t *testing.T) {
servers := map[string]string{"a": "", "b": "state=inactive&ops=10", "c": "ops=20", "d": "group=test1&group=test&ops=20"}
filterByStateAndGroup("test", servers)
if _, ok := servers["b"]; ok {
t.Error("has not remove inactive node")
}
if _, ok := servers["a"]; ok {
t.Error("has not remove inactive node")
}
if _, ok := servers["c"]; ok {
t.Error("has not remove inactive node")
}
if _, ok := servers["d"]; !ok {
t.Error("node must be removed")
}
filterByStateAndGroup("test1", servers)
if _, ok := servers["d"]; !ok {
t.Error("node must be removed")
}
}
func TestUncoverError(t *testing.T) {
var e error = strErr("error")
if uncoverError(e) {
t.Fatalf("expect false but get true")
}
if uncoverError(context.DeadlineExceeded) {
t.Fatalf("expect false but get true")
}
if uncoverError(context.Canceled) {
t.Fatalf("expect false but get true")
}
e = errors.New("error")
if !uncoverError(e) {
t.Fatalf("expect true but get false")
}
}
================================================
FILE: clientplugin/req_rate_limiting_redis.go
================================================
package clientplugin
import (
"context"
"time"
"github.com/go-redis/redis_rate/v10"
"github.com/redis/go-redis/v9"
"github.com/smallnest/rpcx/client"
"github.com/smallnest/rpcx/server"
)
var _ client.PreCallPlugin = (*RedisRateLimitingPlugin)(nil)
// RedisRateLimitingPlugin can limit requests per unit time
type RedisRateLimitingPlugin struct {
addrs []string
limiter redis_rate.Limiter
limit redis_rate.Limit
}
// NewRedisRateLimitingPlugin creates a new RateLimitingPlugin
func NewRedisRateLimitingPlugin(addrs []string, rate int, burst int, period time.Duration) *RedisRateLimitingPlugin {
limit := redis_rate.Limit{
Rate: rate,
Burst: burst,
Period: period,
}
rdb := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: addrs,
})
limiter := redis_rate.NewLimiter(rdb)
return &RedisRateLimitingPlugin{
addrs: addrs,
limiter: *limiter,
limit: limit,
}
}
// PreCall can limit request processing.
func (plugin *RedisRateLimitingPlugin) PreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error {
res, err := plugin.limiter.Allow(ctx, servicePath+"/"+serviceMethod, plugin.limit)
if err != nil {
return err
}
if res.Allowed > 0 {
return nil
}
return server.ErrReqReachLimit
}
================================================
FILE: codec/codec.go
================================================
package codec
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
pb "google.golang.org/protobuf/proto"
"github.com/apache/thrift/lib/go/thrift"
"github.com/gogo/protobuf/proto"
"github.com/tinylib/msgp/msgp"
"github.com/vmihailenco/msgpack/v5"
)
// Codec defines the interface that decode/encode payload.
type Codec interface {
Encode(i interface{}) ([]byte, error)
Decode(data []byte, i interface{}) error
}
// ByteCodec uses raw slice pf bytes and don't encode/decode.
type ByteCodec struct{}
// Encode returns raw slice of bytes.
func (c ByteCodec) Encode(i interface{}) ([]byte, error) {
if data, ok := i.([]byte); ok {
return data, nil
}
if data, ok := i.(*[]byte); ok {
return *data, nil
}
return nil, fmt.Errorf("%T is not a []byte", i)
}
// Decode returns raw slice of bytes.
func (c ByteCodec) Decode(data []byte, i interface{}) error {
reflect.Indirect(reflect.ValueOf(i)).SetBytes(data)
return nil
}
// JSONCodec uses json marshaler and unmarshaler.
type JSONCodec struct{}
// Encode encodes an object into slice of bytes.
func (c JSONCodec) Encode(i interface{}) ([]byte, error) {
return json.Marshal(i)
}
// Decode decodes an object from slice of bytes.
func (c JSONCodec) Decode(data []byte, i interface{}) error {
d := json.NewDecoder(bytes.NewBuffer(data))
d.UseNumber()
return d.Decode(i)
}
// PBCodec uses protobuf marshaler and unmarshaler.
type PBCodec struct{}
// Encode encodes an object into slice of bytes.
func (c PBCodec) Encode(i interface{}) ([]byte, error) {
if m, ok := i.(proto.Marshaler); ok {
return m.Marshal()
}
if m, ok := i.(pb.Message); ok {
return pb.Marshal(m)
}
return nil, fmt.Errorf("%T is not a proto.Marshaler or pb.Message", i)
}
// Decode decodes an object from slice of bytes.
func (c PBCodec) Decode(data []byte, i interface{}) error {
if m, ok := i.(proto.Unmarshaler); ok {
return m.Unmarshal(data)
}
if m, ok := i.(pb.Message); ok {
return pb.Unmarshal(data, m)
}
return fmt.Errorf("%T is not a proto.Unmarshaler or pb.Message", i)
}
// MsgpackCodec uses messagepack marshaler and unmarshaler.
type MsgpackCodec struct{}
// Encode encodes an object into slice of bytes.
func (c MsgpackCodec) Encode(i interface{}) ([]byte, error) {
if m, ok := i.(msgp.Marshaler); ok {
return m.MarshalMsg(nil)
}
var buf bytes.Buffer
enc := msgpack.NewEncoder(&buf)
// enc.UseJSONTag(true)
err := enc.Encode(i)
return buf.Bytes(), err
}
// Decode decodes an object from slice of bytes.
func (c MsgpackCodec) Decode(data []byte, i interface{}) error {
if m, ok := i.(msgp.Unmarshaler); ok {
_, err := m.UnmarshalMsg(data)
return err
}
dec := msgpack.NewDecoder(bytes.NewReader(data))
// dec.UseJSONTag(true)
err := dec.Decode(i)
return err
}
type ThriftCodec struct{}
func (c ThriftCodec) Encode(i interface{}) ([]byte, error) {
b := thrift.NewTMemoryBufferLen(1024)
p := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{}).
GetProtocol(b)
t := &thrift.TSerializer{
Transport: b,
Protocol: p,
}
t.Transport.Close()
if msg, ok := i.(thrift.TStruct); ok {
return t.Write(context.Background(), msg)
}
return nil, errors.New("type assertion failed")
}
func (c ThriftCodec) Decode(data []byte, i interface{}) error {
t := thrift.NewTMemoryBufferLen(1024)
p := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{}).
GetProtocol(t)
d := &thrift.TDeserializer{
Transport: t,
Protocol: p,
}
d.Transport.Close()
return d.Read(context.Background(), i.(thrift.TStruct), data)
}
================================================
FILE: codec/codec_test.go
================================================
package codec
import (
"testing"
"github.com/smallnest/rpcx/codec/testdata"
)
type ColorGroup struct {
Id int `json:"id" xml:"id,attr" msg:"id"`
Name string `json:"name" xml:"name" msg:"name"`
Colors []string `json:"colors" xml:"colors" msg:"colors"`
}
var group = ColorGroup{
Id: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
func BenchmarkJSONCodec_Encode(b *testing.B) {
var raw = make([]byte, 0, 1024)
serializer := JSONCodec{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw, _ = serializer.Encode(group)
}
b.ReportMetric(float64(len(raw)), "bytes")
}
func BenchmarkPBCodec_Encode(b *testing.B) {
var raw = make([]byte, 0, 1024)
serializer := PBCodec{}
group := testdata.ProtoColorGroup{
Id: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw, _ = serializer.Encode(&group)
}
b.ReportMetric(float64(len(raw)), "bytes")
}
func BenchmarkMsgpackCodec_Encode(b *testing.B) {
var raw = make([]byte, 0, 1024)
serializer := MsgpackCodec{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw, _ = serializer.Encode(group)
}
b.ReportMetric(float64(len(raw)), "bytes")
}
func BenchmarkThriftCodec_Encode(b *testing.B) {
var bb = make([]byte, 0, 1024)
serializer := ThriftCodec{}
thriftColorGroup := testdata.ThriftColorGroup{
ID: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
bb, _ = serializer.Encode(&thriftColorGroup)
}
b.ReportMetric(float64(len(bb)), "bytes")
}
func BenchmarkJSONCodec_Decode(b *testing.B) {
serializer := JSONCodec{}
bytes, _ := serializer.Encode(group)
result := ColorGroup{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = serializer.Decode(bytes, &result)
}
}
func BenchmarkPBCodec_Decode(b *testing.B) {
serializer := PBCodec{}
bytes, _ := serializer.Encode(group)
result := ColorGroup{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = serializer.Decode(bytes, &result)
}
}
func BenchmarkMsgpackCodec_Decode(b *testing.B) {
serializer := MsgpackCodec{}
bytes, _ := serializer.Encode(group)
result := ColorGroup{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = serializer.Decode(bytes, &result)
}
}
func BenchmarkThriftCodec_Decode(b *testing.B) {
serializer := ThriftCodec{}
thriftColorGroup := testdata.ThriftColorGroup{
ID: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
bytes, _ := serializer.Encode(&thriftColorGroup)
result := testdata.ThriftColorGroup{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = serializer.Decode(bytes, &result)
}
}
================================================
FILE: codec/testdata/GoUnusedProtection__.go
================================================
// Code generated by Thrift Compiler (0.14.0). DO NOT EDIT.
package testdata
var GoUnusedProtection__ int;
================================================
FILE: codec/testdata/gen.sh
================================================
# generate .go files from IDL
protoc -I. --go_out=. --go_opt=module="testdata" ./protobuf.proto
thrift -r -out ../ --gen go ./thrift_colorgroup.thrift
# # run benchmarks
# go test -bench=. -run=none
# # clean files
# rm -rf ./testdata/*.go
================================================
FILE: codec/testdata/protobuf.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc v5.26.0
// source: protobuf.proto
package testdata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ProtoColorGroup struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Colors []string `protobuf:"bytes,3,rep,name=colors,proto3" json:"colors,omitempty"`
}
func (x *ProtoColorGroup) Reset() {
*x = ProtoColorGroup{}
if protoimpl.UnsafeEnabled {
mi := &file_protobuf_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ProtoColorGroup) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoColorGroup) ProtoMessage() {}
func (x *ProtoColorGroup) ProtoReflect() protoreflect.Message {
mi := &file_protobuf_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoColorGroup.ProtoReflect.Descriptor instead.
func (*ProtoColorGroup) Descriptor() ([]byte, []int) {
return file_protobuf_proto_rawDescGZIP(), []int{0}
}
func (x *ProtoColorGroup) GetId() int32 {
if x != nil {
return x.Id
}
return 0
}
func (x *ProtoColorGroup) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ProtoColorGroup) GetColors() []string {
if x != nil {
return x.Colors
}
return nil
}
var File_protobuf_proto protoreflect.FileDescriptor
var file_protobuf_proto_rawDesc = []byte{
0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x08, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4d, 0x0a, 0x0f, 0x50, 0x72,
0x6f, 0x74, 0x6f, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a,
0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
0x09, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x74,
0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_protobuf_proto_rawDescOnce sync.Once
file_protobuf_proto_rawDescData = file_protobuf_proto_rawDesc
)
func file_protobuf_proto_rawDescGZIP() []byte {
file_protobuf_proto_rawDescOnce.Do(func() {
file_protobuf_proto_rawDescData = protoimpl.X.CompressGZIP(file_protobuf_proto_rawDescData)
})
return file_protobuf_proto_rawDescData
}
var file_protobuf_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_protobuf_proto_goTypes = []interface{}{
(*ProtoColorGroup)(nil), // 0: testdata.ProtoColorGroup
}
var file_protobuf_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_protobuf_proto_init() }
func file_protobuf_proto_init() {
if File_protobuf_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_protobuf_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ProtoColorGroup); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_protobuf_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_protobuf_proto_goTypes,
DependencyIndexes: file_protobuf_proto_depIdxs,
MessageInfos: file_protobuf_proto_msgTypes,
}.Build()
File_protobuf_proto = out.File
file_protobuf_proto_rawDesc = nil
file_protobuf_proto_goTypes = nil
file_protobuf_proto_depIdxs = nil
}
================================================
FILE: codec/testdata/protobuf.proto
================================================
syntax = "proto3";
package testdata;
option go_package = "./testdata";
message ProtoColorGroup {
int32 id = 1;
string name = 2;
repeated string colors = 3;
}
================================================
FILE: codec/testdata/thrift_colorgroup-consts.go
================================================
// Code generated by Thrift Compiler (0.14.0). DO NOT EDIT.
package testdata
import(
"bytes"
"context"
"fmt"
"time"
"github.com/apache/thrift/lib/go/thrift"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = context.Background
var _ = time.Now
var _ = bytes.Equal
func init() {
}
================================================
FILE: codec/testdata/thrift_colorgroup.go
================================================
// Code generated by Thrift Compiler (0.14.0). DO NOT EDIT.
package testdata
import(
"bytes"
"context"
"fmt"
"time"
"github.com/apache/thrift/lib/go/thrift"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = context.Background
var _ = time.Now
var _ = bytes.Equal
// Attributes:
// - ID
// - Name
// - Colors
type ThriftColorGroup struct {
ID int32 `thrift:"id,1" db:"id" json:"id"`
Name string `thrift:"name,2" db:"name" json:"name"`
Colors []string `thrift:"colors,3" db:"colors" json:"colors"`
}
func NewThriftColorGroup() *ThriftColorGroup {
return &ThriftColorGroup{}
}
func (p *ThriftColorGroup) GetID() int32 {
return p.ID
}
func (p *ThriftColorGroup) GetName() string {
return p.Name
}
func (p *ThriftColorGroup) GetColors() []string {
return p.Colors
}
func (p *ThriftColorGroup) Read(ctx context.Context, iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx)
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP { break; }
switch fieldId {
case 1:
if fieldTypeId == thrift.I32 {
if err := p.ReadField1(ctx, iprot); err != nil {
return err
}
} else {
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
case 2:
if fieldTypeId == thrift.STRING {
if err := p.ReadField2(ctx, iprot); err != nil {
return err
}
} else {
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
case 3:
if fieldTypeId == thrift.LIST {
if err := p.ReadField3(ctx, iprot); err != nil {
return err
}
} else {
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
default:
if err := iprot.Skip(ctx, fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(ctx); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *ThriftColorGroup) ReadField1(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadI32(ctx); err != nil {
return thrift.PrependError("error reading field 1: ", err)
} else {
p.ID = v
}
return nil
}
func (p *ThriftColorGroup) ReadField2(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadString(ctx); err != nil {
return thrift.PrependError("error reading field 2: ", err)
} else {
p.Name = v
}
return nil
}
func (p *ThriftColorGroup) ReadField3(ctx context.Context, iprot thrift.TProtocol) error {
_, size, err := iprot.ReadListBegin(ctx)
if err != nil {
return thrift.PrependError("error reading list begin: ", err)
}
tSlice := make([]string, 0, size)
p.Colors = tSlice
for i := 0; i < size; i ++ {
var _elem0 string
if v, err := iprot.ReadString(ctx); err != nil {
return thrift.PrependError("error reading field 0: ", err)
} else {
_elem0 = v
}
p.Colors = append(p.Colors, _elem0)
}
if err := iprot.ReadListEnd(ctx); err != nil {
return thrift.PrependError("error reading list end: ", err)
}
return nil
}
func (p *ThriftColorGroup) Write(ctx context.Context, oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin(ctx, "ThriftColorGroup"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) }
if p != nil {
if err := p.writeField1(ctx, oprot); err != nil { return err }
if err := p.writeField2(ctx, oprot); err != nil { return err }
if err := p.writeField3(ctx, oprot); err != nil { return err }
}
if err := oprot.WriteFieldStop(ctx); err != nil {
return thrift.PrependError("write field stop error: ", err) }
if err := oprot.WriteStructEnd(ctx); err != nil {
return thrift.PrependError("write struct stop error: ", err) }
return nil
}
func (p *ThriftColorGroup) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin(ctx, "id", thrift.I32, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:id: ", p), err) }
if err := oprot.WriteI32(ctx, int32(p.ID)); err != nil {
return thrift.PrependError(fmt.Sprintf("%T.id (1) field write error: ", p), err) }
if err := oprot.WriteFieldEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:id: ", p), err) }
return err
}
func (p *ThriftColorGroup) writeField2(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin(ctx, "name", thrift.STRING, 2); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:name: ", p), err) }
if err := oprot.WriteString(ctx, string(p.Name)); err != nil {
return thrift.PrependError(fmt.Sprintf("%T.name (2) field write error: ", p), err) }
if err := oprot.WriteFieldEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 2:name: ", p), err) }
return err
}
func (p *ThriftColorGroup) writeField3(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin(ctx, "colors", thrift.LIST, 3); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 3:colors: ", p), err) }
if err := oprot.WriteListBegin(ctx, thrift.STRING, len(p.Colors)); err != nil {
return thrift.PrependError("error writing list begin: ", err)
}
for _, v := range p.Colors {
if err := oprot.WriteString(ctx, string(v)); err != nil {
return thrift.PrependError(fmt.Sprintf("%T. (0) field write error: ", p), err) }
}
if err := oprot.WriteListEnd(ctx); err != nil {
return thrift.PrependError("error writing list end: ", err)
}
if err := oprot.WriteFieldEnd(ctx); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 3:colors: ", p), err) }
return err
}
func (p *ThriftColorGroup) Equals(other *ThriftColorGroup) bool {
if p == other {
return true
} else if p == nil || other == nil {
return false
}
if p.ID != other.ID { return false }
if p.Name != other.Name { return false }
if len(p.Colors) != len(other.Colors) { return false }
for i, _tgt := range p.Colors {
_src1 := other.Colors[i]
if _tgt != _src1 { return false }
}
return true
}
func (p *ThriftColorGroup) String() string {
if p == nil {
return ""
}
return fmt.Sprintf("ThriftColorGroup(%+v)", *p)
}
================================================
FILE: codec/testdata/thrift_colorgroup.thrift
================================================
namespace go testdata
struct ThriftColorGroup {
1: i32 id = 0,
2: string name,
3: list colors,
}
================================================
FILE: errors/error.go
================================================
package errors
import (
"fmt"
"sync"
)
// MultiError holds multiple errors
type MultiError struct {
Errors []error
mu sync.Mutex
}
// Error returns the message of the actual error
func (e *MultiError) Error() string {
return fmt.Sprintf("%v", e.Errors)
}
func (e *MultiError) Append(err error) {
e.mu.Lock()
defer e.mu.Unlock()
e.Errors = append(e.Errors, err)
}
func (e *MultiError) ErrorOrNil() error {
if e == nil || len(e.Errors) == 0 {
return nil
}
return e
}
// NewMultiError creates and returns an Error with error splice
func NewMultiError(errors []error) *MultiError {
return &MultiError{Errors: errors}
}
================================================
FILE: errors/error_test.go
================================================
package errors
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewMultiError(t *testing.T) {
var errorSet []error
errorSet = append(errorSet, errors.New("invalid"))
errorSet = append(errorSet, errors.New("fatal"))
multiError := NewMultiError(errorSet)
assert.Equal(t, fmt.Sprintf("%v", errorSet), multiError.Error(), "Test NewMultiError()")
}
func TestMultiError_Append(t *testing.T) {
multiErrors := MultiError{}
multiErrors.Errors = append(multiErrors.Errors, errors.New("invalid"))
multiErrors.Errors = append(multiErrors.Errors, errors.New("fatal"))
assert.Equal(t, 2, len(multiErrors.Errors), "Test Append()")
}
func TestMultiError_Error(t *testing.T) {
multiErrors := MultiError{}
multiErrors.Errors = append(multiErrors.Errors, errors.New("invalid"))
multiErrors.Errors = append(multiErrors.Errors, errors.New("fatal"))
assert.Equal(t, "[invalid fatal]", multiErrors.Error(), "Test Error()")
}
func TestMultiError_ErrorOrNil(t *testing.T) {
multiErrors := MultiError{}
assert.Equal(t, nil, multiErrors.ErrorOrNil(), "Test ErrorOrNil() nil")
multiErrors.Errors = append(multiErrors.Errors, errors.New("invalid"))
multiErrors.Errors = append(multiErrors.Errors, errors.New("fatal"))
assert.Equal(t, &multiErrors, multiErrors.ErrorOrNil(), "Test ErrorOrNil() error")
}
================================================
FILE: go.mod
================================================
module github.com/smallnest/rpcx
go 1.24
require (
github.com/akutz/memconn v0.1.0
github.com/alitto/pond v1.9.2
github.com/apache/thrift v0.21.0
github.com/edwingeng/doublejump v1.0.1
github.com/fatih/color v1.18.0
github.com/go-ping/ping v1.2.0
github.com/go-redis/redis_rate/v10 v10.0.1
github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5
github.com/gogo/protobuf v1.3.2
github.com/golang/snappy v0.0.4
github.com/grandcat/zeroconf v1.0.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/golang-lru v1.0.2
github.com/juju/ratelimit v1.0.2
github.com/julienschmidt/httprouter v1.3.0
github.com/kavu/go_reuseport v1.5.0
github.com/kr/pretty v0.3.1
github.com/quic-go/quic-go v0.57.0
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/redis/go-redis/v9 v9.7.3
github.com/rpcxio/libkv v0.5.1
github.com/rs/cors v1.11.1
github.com/rubyist/circuitbreaker v2.2.1+incompatible
github.com/smallnest/quick v0.2.0
github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a
github.com/soheilhy/cmux v0.1.5
github.com/stretchr/testify v1.11.1
github.com/tinylib/msgp v1.2.5
github.com/twpayne/go-jsonstruct/v3 v3.1.0
github.com/valyala/fastrand v1.1.0
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/xtaci/kcp-go v5.4.20+incompatible
golang.org/x/net v0.43.0
golang.org/x/sync v0.16.0
google.golang.org/protobuf v1.36.4
)
require (
github.com/cenk/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/reedsolomon v1.12.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/libp2p/go-sockaddr v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/peterbourgon/g2s v0.0.0-20140925154142-ec76db4c1ac1 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs=
github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.6/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenk/backoff v2.2.1+incompatible h1:djdFT7f4gF2ttuzRKPbMOWgZajgesItGLwG5FTQKmmE=
github.com/cenk/backoff v2.2.1+incompatible/go.mod h1:7FtoeaSnHoZnmZzz47cM35Y9nSW7tNyaidugnHTaFDE=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-jump v0.0.0-20170409065014-e1f439676b57/go.mod h1:4hKCXuwrJoYvHZxJ86+bRVTOMyJ0Ej+RqfSm8mHi6KA=
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 h1:0wH6nO9QEa02Qx8sIQGw6ieKdz+BXjpccSOo9vXNl4U=
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0/go.mod h1:4hKCXuwrJoYvHZxJ86+bRVTOMyJ0Ej+RqfSm8mHi6KA=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/edwingeng/doublejump v1.0.1 h1:wJ6QgNyyF23Of9vw+ThbwJ/obe9KdxaWEg/Brpv5S1o=
github.com/edwingeng/doublejump v1.0.1/go.mod h1:ykMWX8JWePtMtk2OGjNE9kwtgpI+SF2FNIyXV4gS36k=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/go-redis/redis/v8 v8.8.2/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-redis/redis_rate/v10 v10.0.1 h1:calPxi7tVlxojKunJwQ72kwfozdy25RjA0bCj1h0MUo=
github.com/go-redis/redis_rate/v10 v10.0.1/go.mod h1:EMiuO9+cjRkR7UvdvwMO7vbgqJkltQHtwbdIQvaBKIU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5 h1:5zELAgnSz0gqmr4Q5DWCoOzNHoeBAxVUXB7LS1eG+sw=
github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5/go.mod h1:ermjEDUoT/fS+3Ona5Vd6t6mZkw1eHp99ILO5jGRBkM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/hashicorp/consul/api v1.8.1/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk=
github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk=
github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libp2p/go-sockaddr v0.2.0 h1:Alhhj6lGxVAon9O32tOO89T601EugSx6YiGjy5BVjWk=
github.com/libp2p/go-sockaddr v0.2.0/go.mod h1:5NxulaB17yJ07IpzRIleys4un0PJ7WLWgMDLBBWrGw8=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/peterbourgon/g2s v0.0.0-20140925154142-ec76db4c1ac1 h1:5Dl+ADmsGerAqHwWzyLqkNaUBQ+48DQwfDCaW1gHAQM=
github.com/peterbourgon/g2s v0.0.0-20140925154142-ec76db4c1ac1/go.mod h1:1VcHEd3ro4QMoHfiNl/j7Jkln9+KQuorp0PItHMJYNg=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rpcxio/libkv v0.5.1 h1:M0/QqwTcdXz7us0NB+2i8Kq5+wikTm7zZ4Hyb/jNgME=
github.com/rpcxio/libkv v0.5.1/go.mod h1:zHGgtLr3cFhGtbalum0BrMPOjhFZFJXCKiws/25ewls=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rubyist/circuitbreaker v2.2.1+incompatible h1:KUKd/pV8Geg77+8LNDwdow6rVCAYOp8+kHUyFvL6Mhk=
github.com/rubyist/circuitbreaker v2.2.1+incompatible/go.mod h1:Ycs3JgJADPuzJDwffe12k6BZT8hxVi6lFK+gWYJLN4A=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20201211165307-7117e9ea2414/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smallnest/quick v0.2.0 h1:AEvm7ZovZ6Utv+asFDBh866G4ufMNhRNMKbZHVMFYPE=
github.com/smallnest/quick v0.2.0/go.mod h1:ODNivpfZTaMgYrNb/fhDtqoEe2TTPxSRo8JaIT/QThI=
github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a h1:GI6kCNC5AVFbKA6ZKbVd4r+fk+Z7XZCRQm9LURZY4t4=
github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a/go.mod h1:VJeIKKrDEzT4ZNVe87JN9uRLw1XLp/ZnnE9PfsyJ1jY=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twpayne/go-jsonstruct/v3 v3.1.0 h1:NDK73LAXyMKW/FlkxopMKjOcIGw5HgV0ayUWS1xll68=
github.com/twpayne/go-jsonstruct/v3 v3.1.0/go.mod h1:2pXzrqn1yuRpon60R7JTPQTxYBiWLMSsrh/CDmk1i0Q=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xtaci/kcp-go v5.4.20+incompatible h1:TN1uey3Raw0sTz0Fg8GkfM0uH3YwzhnZWQ1bABv5xAg=
github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=
go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc=
go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA=
go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
================================================
FILE: log/default_logger.go
================================================
package log
import (
"fmt"
"io"
"log"
"os"
"strconv"
"github.com/fatih/color"
)
// The default logger output at level error, to turn everything on like this:
// export RPCX_LOG_LEVEL=99
// type of log level
type Level int
// log level constant
const (
LvPanic Level = iota
LvFatal
LvError
LvWarn
LvInfo
LvDebug
LvMax
)
type outputFn func(calldepth int, s string) error
func dropOutput(calldepth int, s string) error {
return nil
}
type defaultLogger struct {
*log.Logger
out [LvMax]outputFn
}
func NewDefaultLogger(out io.Writer, prefix string, flag int, lv Level) *defaultLogger {
// set default level to error
level := lv
// read level from system enviroment to override
levelStr := os.Getenv("RPCX_LOG_LEVEL")
if len(levelStr) != 0 {
if vl, err := strconv.Atoi(levelStr); err == nil {
level = Level(vl)
}
}
l := &defaultLogger{}
l.Logger = log.New(out, prefix, flag)
for i := int(LvFatal); i < int(LvMax); i++ {
// always enable fatal and panic
// enable only lv <= RPCX_LOG_LEVEL
if i <= int(level) {
l.out[i] = l.Output
} else {
l.out[i] = dropOutput
}
}
return l
}
func (l *defaultLogger) Debug(v ...interface{}) {
_ = l.out[int(LvDebug)](calldepth, header("DEBUG", fmt.Sprint(v...)))
}
func (l *defaultLogger) Debugf(format string, v ...interface{}) {
_ = l.out[int(LvDebug)](calldepth, header("DEBUG", fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Info(v ...interface{}) {
_ = l.out[int(LvInfo)](calldepth, header(color.GreenString("INFO "), fmt.Sprint(v...)))
}
func (l *defaultLogger) Infof(format string, v ...interface{}) {
_ = l.out[int(LvInfo)](calldepth, header(color.GreenString("INFO "), fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Warn(v ...interface{}) {
_ = l.out[int(LvWarn)](calldepth, header(color.YellowString("WARN "), fmt.Sprint(v...)))
}
func (l *defaultLogger) Warnf(format string, v ...interface{}) {
_ = l.out[int(LvWarn)](calldepth, header(color.YellowString("WARN "), fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Error(v ...interface{}) {
_ = l.out[int(LvError)](calldepth, header(color.RedString("ERROR"), fmt.Sprint(v...)))
}
func (l *defaultLogger) Errorf(format string, v ...interface{}) {
_ = l.out[int(LvError)](calldepth, header(color.RedString("ERROR"), fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Fatal(v ...interface{}) {
_ = l.Logger.Output(calldepth, header(color.MagentaString("FATAL"), fmt.Sprint(v...)))
os.Exit(1)
}
func (l *defaultLogger) Fatalf(format string, v ...interface{}) {
_ = l.Logger.Output(calldepth, header(color.MagentaString("FATAL"), fmt.Sprintf(format, v...)))
os.Exit(1)
}
func (l *defaultLogger) Panic(v ...interface{}) {
l.Logger.Panic(v...)
}
func (l *defaultLogger) Panicf(format string, v ...interface{}) {
l.Logger.Panicf(format, v...)
}
func header(lvl, msg string) string {
return fmt.Sprintf("%s: %s", lvl, msg)
}
================================================
FILE: log/dummy_logger.go
================================================
package log
type dummyLogger struct{}
func (l *dummyLogger) Debug(v ...interface{}) {
}
func (l *dummyLogger) Debugf(format string, v ...interface{}) {
}
func (l *dummyLogger) Info(v ...interface{}) {
}
func (l *dummyLogger) Infof(format string, v ...interface{}) {
}
func (l *dummyLogger) Warn(v ...interface{}) {
}
func (l *dummyLogger) Warnf(format string, v ...interface{}) {
}
func (l *dummyLogger) Error(v ...interface{}) {
}
func (l *dummyLogger) Errorf(format string, v ...interface{}) {
}
func (l *dummyLogger) Fatal(v ...interface{}) {
}
func (l *dummyLogger) Fatalf(format string, v ...interface{}) {
}
func (l *dummyLogger) Panic(v ...interface{}) {
}
func (l *dummyLogger) Panicf(format string, v ...interface{}) {
}
================================================
FILE: log/logger.go
================================================
package log
import (
"log"
"os"
)
const (
calldepth = 3
)
var l Logger = NewDefaultLogger(os.Stdout, "", log.LstdFlags|log.Lshortfile, LvError)
type Logger interface {
Debug(v ...interface{})
Debugf(format string, v ...interface{})
Info(v ...interface{})
Infof(format string, v ...interface{})
Warn(v ...interface{})
Warnf(format string, v ...interface{})
Error(v ...interface{})
Errorf(format string, v ...interface{})
Fatal(v ...interface{})
Fatalf(format string, v ...interface{})
Panic(v ...interface{})
Panicf(format string, v ...interface{})
}
func SetLogger(logger Logger) {
l = logger
}
func GetLogger() Logger {
return l
}
func SetDummyLogger() {
l = &dummyLogger{}
}
func Debug(v ...interface{}) {
l.Debug(v...)
}
func Debugf(format string, v ...interface{}) {
l.Debugf(format, v...)
}
func Info(v ...interface{}) {
l.Info(v...)
}
func Infof(format string, v ...interface{}) {
l.Infof(format, v...)
}
func Warn(v ...interface{}) {
l.Warn(v...)
}
func Warnf(format string, v ...interface{}) {
l.Warnf(format, v...)
}
func Error(v ...interface{}) {
l.Error(v...)
}
func Errorf(format string, v ...interface{}) {
l.Errorf(format, v...)
}
func Fatal(v ...interface{}) {
l.Fatal(v...)
}
func Fatalf(format string, v ...interface{}) {
l.Fatalf(format, v...)
}
func Panic(v ...interface{}) {
l.Panic(v...)
}
func Panicf(format string, v ...interface{}) {
l.Panicf(format, v...)
}
================================================
FILE: protocol/compressor.go
================================================
package protocol
import (
"bytes"
"io"
"github.com/golang/snappy"
"github.com/smallnest/rpcx/util"
)
// Compressor defines a common compression interface.
type Compressor interface {
Zip([]byte) ([]byte, error)
Unzip([]byte) ([]byte, error)
}
// GzipCompressor implements gzip compressor.
type GzipCompressor struct {
}
func (c GzipCompressor) Zip(data []byte) ([]byte, error) {
return util.Zip(data)
}
func (c GzipCompressor) Unzip(data []byte) ([]byte, error) {
return util.Unzip(data)
}
type RawDataCompressor struct {
}
func (c RawDataCompressor) Zip(data []byte) ([]byte, error) {
return data, nil
}
func (c RawDataCompressor) Unzip(data []byte) ([]byte, error) {
return data, nil
}
// SnappyCompressor implements snappy compressor
type SnappyCompressor struct {
}
func (c *SnappyCompressor) Zip(data []byte) ([]byte, error) {
if len(data) == 0 {
return data, nil
}
var buffer bytes.Buffer
writer := snappy.NewBufferedWriter(&buffer)
_, err := writer.Write(data)
if err != nil {
writer.Close()
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (c *SnappyCompressor) Unzip(data []byte) ([]byte, error) {
if len(data) == 0 {
return data, nil
}
reader := snappy.NewReader(bytes.NewReader(data))
out, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return out, err
}
================================================
FILE: protocol/compressor_test.go
================================================
package protocol
import (
"reflect"
"testing"
"github.com/smallnest/rpcx/codec"
"github.com/smallnest/rpcx/protocol/testdata"
)
func newBenchmarkMessage() *testdata.BenchmarkMessage {
var theAnswer = "Answer to the Ultimate Question of Life, the Universe, and Everything"
var args testdata.BenchmarkMessage
v := reflect.ValueOf(&args).Elem()
for k := 0; k < v.NumField(); k++ {
field := v.Field(k)
fieldName := v.Type().Field(k).Name
// filter unexported fields
if fieldName[0] >= 97 && fieldName[0] <= 122 {
continue
}
switch field.Kind() {
case reflect.Int, reflect.Int32, reflect.Int64:
field.SetInt(31415926)
case reflect.Bool:
field.SetBool(true)
case reflect.String:
field.SetString(theAnswer)
}
}
return &args
}
func BenchmarkGzipCompressor_Zip(b *testing.B) {
compressor := GzipCompressor{}
serializer := codec.PBCodec{}
raw, _ := serializer.Encode(newBenchmarkMessage())
zipped := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
zipped, _ = compressor.Zip(raw)
}
b.ReportMetric(float64(len(zipped)), "bytes")
}
func BenchmarkRawDataCompressor_Zip(b *testing.B) {
compressor := RawDataCompressor{}
serializer := codec.PBCodec{}
raw, _ := serializer.Encode(newBenchmarkMessage())
zipped := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
zipped, _ = compressor.Zip(raw)
}
b.ReportMetric(float64(len(zipped)), "bytes")
}
func BenchmarkSnappyCompressor_Zip(b *testing.B) {
compressor := SnappyCompressor{}
serializer := codec.PBCodec{}
raw, _ := serializer.Encode(newBenchmarkMessage())
zipped := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
zipped, _ = compressor.Zip(raw)
}
b.ReportMetric(float64(len(zipped)), "bytes")
}
func BenchmarkGzipCompressor_Unzip(b *testing.B) {
compressor := GzipCompressor{}
serializer := codec.PBCodec{}
raw, _ := serializer.Encode(newBenchmarkMessage())
zipped, _ := compressor.Zip(raw)
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw, _ = compressor.Unzip(zipped)
}
b.ReportMetric(float64(len(raw)), "bytes")
}
func BenchmarkRawDataCompressor_Unzip(b *testing.B) {
compressor := RawDataCompressor{}
serializer := codec.PBCodec{}
raw, _ := serializer.Encode(newBenchmarkMessage())
zipped, _ := compressor.Zip(raw)
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw, _ = compressor.Unzip(zipped)
}
b.ReportMetric(float64(len(raw)), "bytes")
}
func BenchmarkSnappyCompressor_Unzip(b *testing.B) {
compressor := SnappyCompressor{}
serializer := codec.PBCodec{}
raw, _ := serializer.Encode(newBenchmarkMessage())
zipped, _ := compressor.Zip(raw)
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw, _ = compressor.Unzip(zipped)
}
b.ReportMetric(float64(len(raw)), "bytes")
}
================================================
FILE: protocol/message.go
================================================
package protocol
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"runtime"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/util"
)
var bufferPool = util.NewLimitedPool(512, 4096)
// Compressors are compressors supported by rpcx. You can add customized compressor in Compressors.
var Compressors = map[CompressType]Compressor{
None: &RawDataCompressor{},
Gzip: &GzipCompressor{},
}
// MaxMessageLength is the max length of a message.
// Default is 0 that means does not limit length of messages.
// It is used to validate when read messages from io.Reader.
var MaxMessageLength = 0
const (
magicNumber byte = 0x08
)
func MagicNumber() byte {
return magicNumber
}
var (
// ErrMetaKVMissing some keys or values are missing.
ErrMetaKVMissing = errors.New("wrong metadata lines. some keys or values are missing")
// ErrMessageTooLong message is too long
ErrMessageTooLong = errors.New("message is too long")
ErrUnsupportedCompressor = errors.New("unsupported compressor")
)
const (
// ServiceError contains error info of service invocation
ServiceError = "__rpcx_error__"
)
// MessageType is message type of requests and responses.
type MessageType byte
const (
// Request is message type of request
Request MessageType = iota
// Response is message type of response
Response
)
// MessageStatusType is status of messages.
type MessageStatusType byte
const (
// Normal is normal requests and responses.
Normal MessageStatusType = iota
// Error indicates some errors occur.
Error
)
// CompressType defines decompression type.
type CompressType byte
const (
// None does not compress.
None CompressType = iota
// Gzip uses gzip compression.
Gzip
)
// SerializeType defines serialization type of payload.
type SerializeType byte
const (
// SerializeNone uses raw []byte and don't serialize/deserialize
SerializeNone SerializeType = iota
// JSON for payload.
JSON
// ProtoBuffer for payload.
ProtoBuffer
// MsgPack for payload
MsgPack
// Thrift
// Thrift for payload
Thrift
)
// Message is the generic type of Request and Response.
type Message struct {
*Header
ServicePath string
ServiceMethod string
Metadata map[string]string
Payload []byte
data []byte
}
// NewMessage creates an empty message.
func NewMessage() *Message {
header := Header([12]byte{})
header[0] = magicNumber
return &Message{
Header: &header,
}
}
// Header is the first part of Message and has fixed size.
// Format:
type Header [12]byte
// CheckMagicNumber checks whether header starts rpcx magic number.
func (h Header) CheckMagicNumber() bool {
return h[0] == magicNumber
}
// Version returns version of rpcx protocol.
func (h Header) Version() byte {
return h[1]
}
// SetVersion sets version for this header.
func (h *Header) SetVersion(v byte) {
h[1] = v
}
// MessageType returns the message type.
func (h Header) MessageType() MessageType {
return MessageType(h[2]&0x80) >> 7
}
// SetMessageType sets message type.
func (h *Header) SetMessageType(mt MessageType) {
h[2] = h[2] | (byte(mt) << 7)
}
// IsHeartbeat returns whether the message is heartbeat message.
func (h Header) IsHeartbeat() bool {
return h[2]&0x40 == 0x40
}
// SetHeartbeat sets the heartbeat flag.
func (h *Header) SetHeartbeat(hb bool) {
if hb {
h[2] = h[2] | 0x40
} else {
h[2] = h[2] &^ 0x40
}
}
// IsOneway returns whether the message is one-way message.
// If true, server won't send responses.
func (h Header) IsOneway() bool {
return h[2]&0x20 == 0x20
}
// SetOneway sets the oneway flag.
func (h *Header) SetOneway(oneway bool) {
if oneway {
h[2] = h[2] | 0x20
} else {
h[2] = h[2] &^ 0x20
}
}
// CompressType returns compression type of messages.
func (h Header) CompressType() CompressType {
return CompressType((h[2] & 0x1C) >> 2)
}
// SetCompressType sets the compression type.
func (h *Header) SetCompressType(ct CompressType) {
h[2] = (h[2] &^ 0x1C) | ((byte(ct) << 2) & 0x1C)
}
// MessageStatusType returns the message status type.
func (h Header) MessageStatusType() MessageStatusType {
return MessageStatusType(h[2] & 0x03)
}
// SetMessageStatusType sets message status type.
func (h *Header) SetMessageStatusType(mt MessageStatusType) {
h[2] = (h[2] &^ 0x03) | (byte(mt) & 0x03)
}
// SerializeType returns serialization type of payload.
func (h Header) SerializeType() SerializeType {
return SerializeType((h[3] & 0xF0) >> 4)
}
// SetSerializeType sets the serialization type.
func (h *Header) SetSerializeType(st SerializeType) {
h[3] = (h[3] &^ 0xF0) | (byte(st) << 4)
}
// Seq returns sequence number of messages.
func (h Header) Seq() uint64 {
return binary.BigEndian.Uint64(h[4:])
}
// SetSeq sets sequence number.
func (h *Header) SetSeq(seq uint64) {
binary.BigEndian.PutUint64(h[4:], seq)
}
// Clone clones from an message.
func (m Message) Clone() *Message {
header := *m.Header
c := NewMessage()
header.SetCompressType(None)
c.Header = &header
c.ServicePath = m.ServicePath
c.ServiceMethod = m.ServiceMethod
return c
}
// Encode encodes messages.
func (m Message) Encode() []byte {
data := m.EncodeSlicePointer()
return *data
}
// EncodeSlicePointer encodes messages as a byte slice pointer we can use pool to improve.
func (m Message) EncodeSlicePointer() *[]byte {
var bb = bytes.NewBuffer(make([]byte, 0, len(m.Metadata)*64))
encodeMetadata(m.Metadata, bb)
meta := bb.Bytes()
spL := len(m.ServicePath)
smL := len(m.ServiceMethod)
var err error
payload := m.Payload
if m.CompressType() != None {
compressor := Compressors[m.CompressType()]
if compressor == nil {
m.SetCompressType(None)
} else {
payload, err = compressor.Zip(m.Payload)
if err != nil {
m.SetCompressType(None)
payload = m.Payload
}
}
}
totalL := (4 + spL) + (4 + smL) + (4 + len(meta)) + (4 + len(payload))
// header + dataLen + spLen + sp + smLen + sm + metaL + meta + payloadLen + payload
metaStart := 12 + 4 + (4 + spL) + (4 + smL)
payLoadStart := metaStart + (4 + len(meta))
l := 12 + 4 + totalL
data := bufferPool.Get(l)
copy(*data, m.Header[:])
// totalLen
binary.BigEndian.PutUint32((*data)[12:16], uint32(totalL))
binary.BigEndian.PutUint32((*data)[16:20], uint32(spL))
copy((*data)[20:20+spL], util.StringToSliceByte(m.ServicePath))
binary.BigEndian.PutUint32((*data)[20+spL:24+spL], uint32(smL))
copy((*data)[24+spL:metaStart], util.StringToSliceByte(m.ServiceMethod))
binary.BigEndian.PutUint32((*data)[metaStart:metaStart+4], uint32(len(meta)))
copy((*data)[metaStart+4:], meta)
binary.BigEndian.PutUint32((*data)[payLoadStart:payLoadStart+4], uint32(len(payload)))
copy((*data)[payLoadStart+4:], payload)
return data
}
// PutData puts the byte slice into pool.
func PutData(data *[]byte) {
bufferPool.Put(data)
}
// WriteTo writes message to writers.
func (m Message) WriteTo(w io.Writer) (int64, error) {
nn, err := w.Write(m.Header[:])
n := int64(nn)
if err != nil {
return n, err
}
bb := bytes.NewBuffer(make([]byte, 0, len(m.Metadata)*64))
encodeMetadata(m.Metadata, bb)
meta := bb.Bytes()
spL := len(m.ServicePath)
smL := len(m.ServiceMethod)
payload := m.Payload
if m.CompressType() != None {
compressor := Compressors[m.CompressType()]
if compressor == nil {
return n, ErrUnsupportedCompressor
}
payload, err = compressor.Zip(m.Payload)
if err != nil {
return n, err
}
}
totalL := (4 + spL) + (4 + smL) + (4 + len(meta)) + (4 + len(payload))
err = binary.Write(w, binary.BigEndian, uint32(totalL))
if err != nil {
return n, err
}
// write servicePath and serviceMethod
err = binary.Write(w, binary.BigEndian, uint32(len(m.ServicePath)))
if err != nil {
return n, err
}
_, err = w.Write(util.StringToSliceByte(m.ServicePath))
if err != nil {
return n, err
}
err = binary.Write(w, binary.BigEndian, uint32(len(m.ServiceMethod)))
if err != nil {
return n, err
}
_, err = w.Write(util.StringToSliceByte(m.ServiceMethod))
if err != nil {
return n, err
}
// write meta
err = binary.Write(w, binary.BigEndian, uint32(len(meta)))
if err != nil {
return n, err
}
_, err = w.Write(meta)
if err != nil {
return n, err
}
// write payload
err = binary.Write(w, binary.BigEndian, uint32(len(payload)))
if err != nil {
return n, err
}
nn, err = w.Write(payload)
return int64(nn), err
}
// len,string,len,string,......
func encodeMetadata(m map[string]string, bb *bytes.Buffer) {
if len(m) == 0 {
return
}
d := make([]byte, 4)
for k, v := range m {
binary.BigEndian.PutUint32(d, uint32(len(k)))
bb.Write(d)
bb.Write(util.StringToSliceByte(k))
binary.BigEndian.PutUint32(d, uint32(len(v)))
bb.Write(d)
bb.Write(util.StringToSliceByte(v))
}
}
func decodeMetadata(l uint32, data []byte) (map[string]string, error) {
m := make(map[string]string, 10)
n := uint32(0)
for n < l {
// parse one key and value
// key
sl := binary.BigEndian.Uint32(data[n : n+4])
n = n + 4
if n+sl > l-4 {
return m, ErrMetaKVMissing
}
k := string(data[n : n+sl])
n = n + sl
// value
sl = binary.BigEndian.Uint32(data[n : n+4])
n = n + 4
if n+sl > l {
return m, ErrMetaKVMissing
}
v := string(data[n : n+sl])
n = n + sl
m[k] = v
}
return m, nil
}
// Read reads a message from r.
func Read(r io.Reader) (*Message, error) {
msg := NewMessage()
err := msg.Decode(r)
if err != nil {
return nil, err
}
return msg, nil
}
// Decode decodes a message from reader.
func (m *Message) Decode(r io.Reader) error {
defer func() {
if err := recover(); err != nil {
var errStack = make([]byte, 1024)
n := runtime.Stack(errStack, true)
log.Errorf("panic in message decode: %v, stack: %s", err, errStack[:n])
}
}()
// parse header
_, err := io.ReadFull(r, m.Header[:1])
if err != nil {
return err
}
if !m.Header.CheckMagicNumber() {
return fmt.Errorf("wrong magic number: %v", m.Header[0])
}
_, err = io.ReadFull(r, m.Header[1:])
if err != nil {
return err
}
// total
lenData := make([]byte, 4)
_, err = io.ReadFull(r, lenData)
if err != nil {
return err
}
l := binary.BigEndian.Uint32(lenData)
if MaxMessageLength > 0 && int(l) > MaxMessageLength {
return ErrMessageTooLong
}
totalL := int(l)
if cap(m.data) >= totalL { // reuse data
m.data = m.data[:totalL]
} else {
m.data = make([]byte, totalL)
}
data := m.data
_, err = io.ReadFull(r, data)
if err != nil {
return err
}
n := 0
// parse servicePath
l = binary.BigEndian.Uint32(data[n:4])
n = n + 4
nEnd := n + int(l)
m.ServicePath = util.SliceByteToString(data[n:nEnd])
n = nEnd
// parse serviceMethod
l = binary.BigEndian.Uint32(data[n : n+4])
n = n + 4
nEnd = n + int(l)
m.ServiceMethod = util.SliceByteToString(data[n:nEnd])
n = nEnd
// parse meta
l = binary.BigEndian.Uint32(data[n : n+4])
n = n + 4
nEnd = n + int(l)
if l > 0 {
m.Metadata, err = decodeMetadata(l, data[n:nEnd])
if err != nil {
return err
}
}
n = nEnd
// parse payload
l = binary.BigEndian.Uint32(data[n : n+4])
_ = l
n = n + 4
m.Payload = data[n:]
if m.CompressType() != None {
compressor := Compressors[m.CompressType()]
if compressor == nil {
return ErrUnsupportedCompressor
}
m.Payload, err = compressor.Unzip(m.Payload)
if err != nil {
return err
}
}
return err
}
// Reset clean data of this message but keep allocated data
func (m *Message) Reset() {
resetHeader(m.Header)
m.Metadata = nil
m.Payload = []byte{}
m.data = m.data[:0]
m.ServicePath = ""
m.ServiceMethod = ""
}
var (
zeroHeaderArray Header
zeroHeader = zeroHeaderArray[1:]
)
func resetHeader(h *Header) {
copy(h[1:], zeroHeader)
}
================================================
FILE: protocol/message_chan_test.go
================================================
package protocol
import (
"fmt"
"strings"
"sync/atomic"
"testing"
"time"
)
func TestChanValue(t *testing.T) {
var ct uint64
ch := make(chan Message, 10000)
go func(ch <-chan Message) {
for range ch {
atomic.AddUint64(&ct, 1)
}
}(ch)
go func(ch chan Message) {
m := strings.Repeat("_", 100)
p := strings.Repeat("_", 100)
payload := make([]byte, 1024)
for {
ch <- Message{ServiceMethod: m, ServicePath: p, Payload: payload}
}
}(ch)
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println(atomic.LoadUint64(&ct))
atomic.StoreUint64(&ct, 0)
}
}
func TestChanPtr(t *testing.T) {
var ct uint64
ch := make(chan *Message, 10000)
go func(ch <-chan *Message) {
for range ch {
atomic.AddUint64(&ct, 1)
}
}(ch)
go func(ch chan *Message) {
m := strings.Repeat("_", 100)
p := strings.Repeat("_", 100)
payload := make([]byte, 1024)
for {
ch <- &Message{ServiceMethod: m, ServicePath: p,Payload: payload}
}
}(ch)
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println(atomic.LoadUint64(&ct))
atomic.StoreUint64(&ct, 0)
}
}
func BenchmarkChanValue(b *testing.B) {
ch := make(chan Message, 10000)
var ct uint64
go func(ch <-chan Message) {
for range ch {
atomic.AddUint64(&ct, 1)
}
}(ch)
b.ReportAllocs()
b.ResetTimer()
m := strings.Repeat("_", 100)
p := strings.Repeat("_", 100)
payload := make([]byte, 1024)
for i := 0; i < b.N; i++ {
ch <- Message{ServiceMethod: m, ServicePath: p,Payload: payload}
}
}
func BenchmarkChanPtr(b *testing.B) {
ch := make(chan *Message, 10000)
var ct uint64
go func(ch <-chan *Message) {
for range ch {
atomic.AddUint64(&ct, 1)
}
}(ch)
b.ReportAllocs()
b.ResetTimer()
m := strings.Repeat("_", 100)
p := strings.Repeat("_", 100)
payload := make([]byte, 1024)
for i := 0; i < b.N; i++ {
ch <- &Message{ServiceMethod: m, ServicePath: p,Payload: payload}
}
}
================================================
FILE: protocol/message_test.go
================================================
package protocol
import (
"bytes"
"testing"
)
func TestMessage(t *testing.T) {
req := NewMessage()
req.SetVersion(0)
req.SetMessageType(Request)
req.SetHeartbeat(false)
req.SetOneway(false)
req.SetCompressType(None)
req.SetMessageStatusType(Normal)
req.SetSerializeType(JSON)
req.SetSeq(1234567890)
m := make(map[string]string)
req.ServicePath = "Arith"
req.ServiceMethod = "Add"
m["__ID"] = "6ba7b810-9dad-11d1-80b4-00c04fd430c9"
req.Metadata = m
payload := `{
"A": 1,
"B": 2,
}
`
req.Payload = []byte(payload)
var buf bytes.Buffer
_, err := req.WriteTo(&buf)
if err != nil {
t.Fatal(err)
}
res, err := Read(&buf)
if err != nil {
t.Fatal(err)
}
res.SetMessageType(Response)
if res.Version() != 0 {
t.Errorf("expect 0 but got %d", res.Version())
}
if res.Seq() != 1234567890 {
t.Errorf("expect 1234567890 but got %d", res.Seq())
}
if res.ServicePath != "Arith" || res.ServiceMethod != "Add" || res.Metadata["__ID"] != "6ba7b810-9dad-11d1-80b4-00c04fd430c9" {
t.Errorf("got wrong metadata: %v", res.Metadata)
}
if string(res.Payload) != payload {
t.Errorf("got wrong payload: %v", string(res.Payload))
}
}
================================================
FILE: protocol/testdata/benchmark.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc v5.26.0
// source: benchmark.proto
package testdata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type BenchmarkMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Field1 string `protobuf:"bytes,1,opt,name=field1,proto3" json:"field1,omitempty"`
Field9 string `protobuf:"bytes,9,opt,name=field9,proto3" json:"field9,omitempty"`
Field18 string `protobuf:"bytes,18,opt,name=field18,proto3" json:"field18,omitempty"`
Field80 bool `protobuf:"varint,80,opt,name=field80,proto3" json:"field80,omitempty"`
Field81 bool `protobuf:"varint,81,opt,name=field81,proto3" json:"field81,omitempty"`
Field2 int32 `protobuf:"varint,2,opt,name=field2,proto3" json:"field2,omitempty"`
Field3 int32 `protobuf:"varint,3,opt,name=field3,proto3" json:"field3,omitempty"`
Field280 int32 `protobuf:"varint,280,opt,name=field280,proto3" json:"field280,omitempty"`
Field6 int32 `protobuf:"varint,6,opt,name=field6,proto3" json:"field6,omitempty"`
Field22 int64 `protobuf:"varint,22,opt,name=field22,proto3" json:"field22,omitempty"`
Field4 string `protobuf:"bytes,4,opt,name=field4,proto3" json:"field4,omitempty"`
Field5 []uint64 `protobuf:"fixed64,5,rep,packed,name=field5,proto3" json:"field5,omitempty"`
Field59 bool `protobuf:"varint,59,opt,name=field59,proto3" json:"field59,omitempty"`
Field7 string `protobuf:"bytes,7,opt,name=field7,proto3" json:"field7,omitempty"`
Field16 int32 `protobuf:"varint,16,opt,name=field16,proto3" json:"field16,omitempty"`
Field130 int32 `protobuf:"varint,130,opt,name=field130,proto3" json:"field130,omitempty"`
Field12 bool `protobuf:"varint,12,opt,name=field12,proto3" json:"field12,omitempty"`
Field17 bool `protobuf:"varint,17,opt,name=field17,proto3" json:"field17,omitempty"`
Field13 bool `protobuf:"varint,13,opt,name=field13,proto3" json:"field13,omitempty"`
Field14 bool `protobuf:"varint,14,opt,name=field14,proto3" json:"field14,omitempty"`
Field104 int32 `protobuf:"varint,104,opt,name=field104,proto3" json:"field104,omitempty"`
Field100 int32 `protobuf:"varint,100,opt,name=field100,proto3" json:"field100,omitempty"`
Field101 int32 `protobuf:"varint,101,opt,name=field101,proto3" json:"field101,omitempty"`
Field102 string `protobuf:"bytes,102,opt,name=field102,proto3" json:"field102,omitempty"`
Field103 string `protobuf:"bytes,103,opt,name=field103,proto3" json:"field103,omitempty"`
Field29 int32 `protobuf:"varint,29,opt,name=field29,proto3" json:"field29,omitempty"`
Field30 bool `protobuf:"varint,30,opt,name=field30,proto3" json:"field30,omitempty"`
Field60 int32 `protobuf:"varint,60,opt,name=field60,proto3" json:"field60,omitempty"`
Field271 int32 `protobuf:"varint,271,opt,name=field271,proto3" json:"field271,omitempty"`
Field272 int32 `protobuf:"varint,272,opt,name=field272,proto3" json:"field272,omitempty"`
Field150 int32 `protobuf:"varint,150,opt,name=field150,proto3" json:"field150,omitempty"`
Field23 int32 `protobuf:"varint,23,opt,name=field23,proto3" json:"field23,omitempty"`
Field24 bool `protobuf:"varint,24,opt,name=field24,proto3" json:"field24,omitempty"`
Field25 int32 `protobuf:"varint,25,opt,name=field25,proto3" json:"field25,omitempty"`
Field78 bool `protobuf:"varint,78,opt,name=field78,proto3" json:"field78,omitempty"`
Field67 int32 `protobuf:"varint,67,opt,name=field67,proto3" json:"field67,omitempty"`
Field68 int32 `protobuf:"varint,68,opt,name=field68,proto3" json:"field68,omitempty"`
Field128 int32 `protobuf:"varint,128,opt,name=field128,proto3" json:"field128,omitempty"`
Field129 string `protobuf:"bytes,129,opt,name=field129,proto3" json:"field129,omitempty"`
Field131 int32 `protobuf:"varint,131,opt,name=field131,proto3" json:"field131,omitempty"`
}
func (x *BenchmarkMessage) Reset() {
*x = BenchmarkMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_benchmark_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BenchmarkMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BenchmarkMessage) ProtoMessage() {}
func (x *BenchmarkMessage) ProtoReflect() protoreflect.Message {
mi := &file_benchmark_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BenchmarkMessage.ProtoReflect.Descriptor instead.
func (*BenchmarkMessage) Descriptor() ([]byte, []int) {
return file_benchmark_proto_rawDescGZIP(), []int{0}
}
func (x *BenchmarkMessage) GetField1() string {
if x != nil {
return x.Field1
}
return ""
}
func (x *BenchmarkMessage) GetField9() string {
if x != nil {
return x.Field9
}
return ""
}
func (x *BenchmarkMessage) GetField18() string {
if x != nil {
return x.Field18
}
return ""
}
func (x *BenchmarkMessage) GetField80() bool {
if x != nil {
return x.Field80
}
return false
}
func (x *BenchmarkMessage) GetField81() bool {
if x != nil {
return x.Field81
}
return false
}
func (x *BenchmarkMessage) GetField2() int32 {
if x != nil {
return x.Field2
}
return 0
}
func (x *BenchmarkMessage) GetField3() int32 {
if x != nil {
return x.Field3
}
return 0
}
func (x *BenchmarkMessage) GetField280() int32 {
if x != nil {
return x.Field280
}
return 0
}
func (x *BenchmarkMessage) GetField6() int32 {
if x != nil {
return x.Field6
}
return 0
}
func (x *BenchmarkMessage) GetField22() int64 {
if x != nil {
return x.Field22
}
return 0
}
func (x *BenchmarkMessage) GetField4() string {
if x != nil {
return x.Field4
}
return ""
}
func (x *BenchmarkMessage) GetField5() []uint64 {
if x != nil {
return x.Field5
}
return nil
}
func (x *BenchmarkMessage) GetField59() bool {
if x != nil {
return x.Field59
}
return false
}
func (x *BenchmarkMessage) GetField7() string {
if x != nil {
return x.Field7
}
return ""
}
func (x *BenchmarkMessage) GetField16() int32 {
if x != nil {
return x.Field16
}
return 0
}
func (x *BenchmarkMessage) GetField130() int32 {
if x != nil {
return x.Field130
}
return 0
}
func (x *BenchmarkMessage) GetField12() bool {
if x != nil {
return x.Field12
}
return false
}
func (x *BenchmarkMessage) GetField17() bool {
if x != nil {
return x.Field17
}
return false
}
func (x *BenchmarkMessage) GetField13() bool {
if x != nil {
return x.Field13
}
return false
}
func (x *BenchmarkMessage) GetField14() bool {
if x != nil {
return x.Field14
}
return false
}
func (x *BenchmarkMessage) GetField104() int32 {
if x != nil {
return x.Field104
}
return 0
}
func (x *BenchmarkMessage) GetField100() int32 {
if x != nil {
return x.Field100
}
return 0
}
func (x *BenchmarkMessage) GetField101() int32 {
if x != nil {
return x.Field101
}
return 0
}
func (x *BenchmarkMessage) GetField102() string {
if x != nil {
return x.Field102
}
return ""
}
func (x *BenchmarkMessage) GetField103() string {
if x != nil {
return x.Field103
}
return ""
}
func (x *BenchmarkMessage) GetField29() int32 {
if x != nil {
return x.Field29
}
return 0
}
func (x *BenchmarkMessage) GetField30() bool {
if x != nil {
return x.Field30
}
return false
}
func (x *BenchmarkMessage) GetField60() int32 {
if x != nil {
return x.Field60
}
return 0
}
func (x *BenchmarkMessage) GetField271() int32 {
if x != nil {
return x.Field271
}
return 0
}
func (x *BenchmarkMessage) GetField272() int32 {
if x != nil {
return x.Field272
}
return 0
}
func (x *BenchmarkMessage) GetField150() int32 {
if x != nil {
return x.Field150
}
return 0
}
func (x *BenchmarkMessage) GetField23() int32 {
if x != nil {
return x.Field23
}
return 0
}
func (x *BenchmarkMessage) GetField24() bool {
if x != nil {
return x.Field24
}
return false
}
func (x *BenchmarkMessage) GetField25() int32 {
if x != nil {
return x.Field25
}
return 0
}
func (x *BenchmarkMessage) GetField78() bool {
if x != nil {
return x.Field78
}
return false
}
func (x *BenchmarkMessage) GetField67() int32 {
if x != nil {
return x.Field67
}
return 0
}
func (x *BenchmarkMessage) GetField68() int32 {
if x != nil {
return x.Field68
}
return 0
}
func (x *BenchmarkMessage) GetField128() int32 {
if x != nil {
return x.Field128
}
return 0
}
func (x *BenchmarkMessage) GetField129() string {
if x != nil {
return x.Field129
}
return ""
}
func (x *BenchmarkMessage) GetField131() int32 {
if x != nil {
return x.Field131
}
return 0
}
var File_benchmark_proto protoreflect.FileDescriptor
var file_benchmark_proto_rawDesc = []byte{
0x0a, 0x0f, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x08, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb4, 0x08, 0x0a, 0x10,
0x42, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c,
0x64, 0x39, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x39,
0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x38, 0x18, 0x12, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x38, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x38, 0x30, 0x18, 0x50, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x38, 0x30, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x38, 0x31, 0x18,
0x51, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x38, 0x31, 0x12, 0x16,
0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33,
0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x12, 0x1b,
0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x38, 0x30, 0x18, 0x98, 0x02, 0x20, 0x01, 0x28,
0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x38, 0x30, 0x12, 0x16, 0x0a, 0x06, 0x66,
0x69, 0x65, 0x6c, 0x64, 0x36, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x36, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x32, 0x18, 0x16,
0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x32, 0x12, 0x16, 0x0a,
0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x34, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66,
0x69, 0x65, 0x6c, 0x64, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x18,
0x05, 0x20, 0x03, 0x28, 0x06, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x12, 0x18, 0x0a,
0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x39, 0x18, 0x3b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x39, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64,
0x37, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x12,
0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x36, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05,
0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x36, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x31, 0x33, 0x30, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x31, 0x33, 0x30, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31,
0x32, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32,
0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x37, 0x18, 0x11, 0x20, 0x01, 0x28,
0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x37, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x31, 0x33, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x31, 0x33, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x34, 0x18,
0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x34, 0x12, 0x1a,
0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x34, 0x18, 0x68, 0x20, 0x01, 0x28, 0x05,
0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x34, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x31, 0x30, 0x30, 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x31, 0x30, 0x30, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31,
0x30, 0x31, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31,
0x30, 0x31, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x32, 0x18, 0x66,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x32, 0x12, 0x1a,
0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x33, 0x18, 0x67, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x33, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x32, 0x39, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x32, 0x39, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x30, 0x18,
0x1e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x30, 0x12, 0x18,
0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x30, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x05, 0x52,
0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x30, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c,
0x64, 0x32, 0x37, 0x31, 0x18, 0x8f, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x32, 0x37, 0x31, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x37,
0x32, 0x18, 0x90, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32,
0x37, 0x32, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x35, 0x30, 0x18, 0x96,
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x35, 0x30, 0x12,
0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x33, 0x18, 0x17, 0x20, 0x01, 0x28, 0x05,
0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x33, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x32, 0x34, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c,
0x64, 0x32, 0x34, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x35, 0x18, 0x19,
0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x35, 0x12, 0x18, 0x0a,
0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x38, 0x18, 0x4e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x38, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64,
0x36, 0x37, 0x18, 0x43, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36,
0x37, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x38, 0x18, 0x44, 0x20, 0x01,
0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x38, 0x12, 0x1b, 0x0a, 0x08, 0x66,
0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x38, 0x18, 0x80, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x38, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c,
0x64, 0x31, 0x32, 0x39, 0x18, 0x81, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x31, 0x32, 0x39, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33,
0x31, 0x18, 0x83, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31,
0x33, 0x31, 0x42, 0x0e, 0x48, 0x01, 0x5a, 0x0a, 0x2e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61,
0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_benchmark_proto_rawDescOnce sync.Once
file_benchmark_proto_rawDescData = file_benchmark_proto_rawDesc
)
func file_benchmark_proto_rawDescGZIP() []byte {
file_benchmark_proto_rawDescOnce.Do(func() {
file_benchmark_proto_rawDescData = protoimpl.X.CompressGZIP(file_benchmark_proto_rawDescData)
})
return file_benchmark_proto_rawDescData
}
var file_benchmark_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_benchmark_proto_goTypes = []interface{}{
(*BenchmarkMessage)(nil), // 0: testdata.BenchmarkMessage
}
var file_benchmark_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_benchmark_proto_init() }
func file_benchmark_proto_init() {
if File_benchmark_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_benchmark_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BenchmarkMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_benchmark_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_benchmark_proto_goTypes,
DependencyIndexes: file_benchmark_proto_depIdxs,
MessageInfos: file_benchmark_proto_msgTypes,
}.Build()
File_benchmark_proto = out.File
file_benchmark_proto_rawDesc = nil
file_benchmark_proto_goTypes = nil
file_benchmark_proto_depIdxs = nil
}
================================================
FILE: protocol/testdata/benchmark.proto
================================================
syntax = "proto3";
package testdata;
option optimize_for = SPEED;
option go_package = "./testdata";
message BenchmarkMessage {
string field1 = 1;
string field9 = 9;
string field18 = 18;
bool field80 = 80;
bool field81 = 81;
int32 field2 = 2;
int32 field3 = 3;
int32 field280 = 280;
int32 field6 = 6;
int64 field22 = 22;
string field4 = 4;
repeated fixed64 field5 = 5;
bool field59 = 59;
string field7 = 7;
int32 field16 = 16;
int32 field130 = 130;
bool field12 = 12;
bool field17 = 17;
bool field13 = 13;
bool field14 = 14;
int32 field104 = 104;
int32 field100 = 100;
int32 field101 = 101;
string field102 = 102;
string field103 = 103;
int32 field29 = 29;
bool field30 = 30;
int32 field60 = 60;
int32 field271 = 271;
int32 field272 = 272;
int32 field150 = 150;
int32 field23 = 23;
bool field24 = 24;
int32 field25 = 25;
bool field78 = 78;
int32 field67 = 67;
int32 field68 = 68;
int32 field128 = 128;
string field129 = 129;
int32 field131 = 131;
}
================================================
FILE: protocol/testdata/gen.sh
================================================
# curl -O https://raw.githubusercontent.com/rpcxio/rpcx-benchmark/master/proto/benchmark.proto
# generate .go files from IDL
protoc -I. --go_out=. --go_opt=module="testdata" ./benchmark.proto
================================================
FILE: reflection/server_reflection.go
================================================
package reflection
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"strings"
"text/template"
"unicode"
"unicode/utf8"
"github.com/smallnest/rpcx/log"
"github.com/twpayne/go-jsonstruct/v3"
)
var (
typeOfError = reflect.TypeFor[error]()
typeOfContext = reflect.TypeFor[context.Context]()
)
// ServiceInfo service info.
type ServiceInfo struct {
Name string
PkgPath string
Methods []*MethodInfo
}
// MethodInfo method info
type MethodInfo struct {
Name string
ReqName string
Req string
ReplyName string
Reply string
}
var siTemplate = `package {{.PkgPath}}
type {{.Name}} struct{}
{{$name := .Name}}
{{range .Methods}}
{{.Req}}
{{.Reply}}
type (s *{{$name}}) {{.Name}}(ctx context.Context, arg *{{.ReqName}}, reply *{{.ReplyName}}) error {
return nil
}
{{end}}
`
func (si ServiceInfo) String() string {
tpl := template.Must(template.New("service").Parse(siTemplate))
var buf strings.Builder
_ = tpl.Execute(&buf, si)
return buf.String()
}
type Reflection struct {
Services map[string]*ServiceInfo
}
func New() *Reflection {
return &Reflection{
Services: make(map[string]*ServiceInfo),
}
}
func (r *Reflection) Register(name string, rcvr interface{}, metadata string) error {
si := &ServiceInfo{}
val := reflect.ValueOf(rcvr)
typ := reflect.TypeOf(rcvr)
vTyp := reflect.Indirect(val).Type()
si.Name = vTyp.Name()
pkg := vTyp.PkgPath()
if strings.Index(pkg, ".") > 0 {
pkg = pkg[strings.LastIndex(pkg, ".")+1:]
}
pkg = filepath.Base(pkg)
pkg = strings.ReplaceAll(pkg, "-", "_")
si.PkgPath = pkg
for m := 0; m < val.NumMethod(); m++ {
method := typ.Method(m)
mtype := method.Type
if method.PkgPath != "" {
continue
}
if mtype.NumIn() != 4 {
continue
}
// First arg must be context.Context
ctxType := mtype.In(1)
if !ctxType.Implements(typeOfContext) {
continue
}
// Second arg need not be a pointer.
argType := mtype.In(2)
if !isExportedOrBuiltinType(argType) {
continue
}
// Third arg must be a pointer.
replyType := mtype.In(3)
if replyType.Kind() != reflect.Ptr {
continue
}
// Reply type must be exported.
if !isExportedOrBuiltinType(replyType) {
continue
}
// Method needs one out.
if mtype.NumOut() != 1 {
continue
}
// The return type of the method must be error.
if returnType := mtype.Out(0); returnType != typeOfError {
continue
}
mi := &MethodInfo{}
mi.Name = method.Name
if argType.Kind() == reflect.Ptr {
argType = argType.Elem()
}
replyType = replyType.Elem()
mi.ReqName = argType.Name()
mi.Req = generateTypeDefination(mi.ReqName, si.PkgPath, generateJSON(argType))
mi.ReplyName = replyType.Name()
mi.Reply = generateTypeDefination(mi.ReplyName, si.PkgPath, generateJSON(replyType))
si.Methods = append(si.Methods, mi)
}
if len(si.Methods) > 0 {
r.Services[name] = si
}
return nil
}
func (r *Reflection) Unregister(name string) error {
delete(r.Services, name)
return nil
}
func (r *Reflection) GetService(ctx context.Context, s string, reply *string) error {
si, ok := r.Services[s]
if !ok {
return fmt.Errorf("not found service %s", s)
}
*reply = si.String()
return nil
}
func (r *Reflection) GetServices(ctx context.Context, s string, reply *string) error {
var buf strings.Builder
pkg := `package `
for _, si := range r.Services {
if pkg == `package ` {
pkg = pkg + si.PkgPath + "\n\n"
}
buf.WriteString(strings.ReplaceAll(si.String(), pkg, ""))
}
if pkg != `package ` {
*reply = pkg + buf.String()
} else {
*reply = buf.String()
}
return nil
}
func generateTypeDefination(name, pkg string, jsonValue string) string {
jsonValue = strings.TrimSpace(jsonValue)
if jsonValue == "" || jsonValue == `""` {
return ""
}
r := strings.NewReader(jsonValue)
generator := jsonstruct.NewGenerator(
jsonstruct.WithPackageName(pkg),
jsonstruct.WithTypeName(name),
jsonstruct.WithStructTagNames(nil),
jsonstruct.WithIntType("int64"),
)
if err := generator.ObserveJSONReader(r); err != nil {
log.Errorf("failed to observe json: %v", err)
return ""
}
output, err := generator.Generate()
if err != nil {
log.Errorf("failed to generate json: %v", err)
return ""
}
rt := strings.ReplaceAll(string(output), "``", "")
return strings.ReplaceAll(rt, "package "+pkg+"\n\n", "")
}
func generateJSON(typ reflect.Type) string {
v := reflect.New(typ).Interface()
data, _ := json.Marshal(v)
return string(data)
}
func isExported(name string) bool {
rune, _ := utf8.DecodeRuneInString(name)
return unicode.IsUpper(rune)
}
func isExportedOrBuiltinType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return isExported(t.Name()) || t.PkgPath() == ""
}
================================================
FILE: reflection/server_reflection_test.go
================================================
package reflection
import (
"context"
"reflect"
"testing"
"github.com/kr/pretty"
testutils "github.com/smallnest/rpcx/_testutils"
"github.com/stretchr/testify/assert"
)
type PBArith int
func (t *PBArith) Mul(ctx context.Context, args *testutils.ProtoArgs, reply *testutils.ProtoReply) error {
reply.C = args.A * args.B
return nil
}
func TestReflection_Register(t *testing.T) {
r := New()
arith := PBArith(0)
err := r.Register("Arith", &arith, "")
if err != nil {
t.Fatal(err)
}
pretty.Println(r.Services["Arith"].String())
}
type Args struct {
Aa int
B string
C bool
}
func Test_generateJSON(t *testing.T) {
argsType := reflect.TypeOf(&Args{}).Elem()
jsonData := generateJSON(argsType)
assert.Equal(t, `{"Aa":0,"B":"","C":false}`, jsonData)
def := generateTypeDefination("Args", "test", jsonData)
result := "type Args struct {\n\tAa int64 \n\tB string \n\tC bool \n}\n"
assert.Equal(t, result, def)
}
================================================
FILE: server/context.go
================================================
package server
import (
"fmt"
"net"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
)
// Context represents a rpcx FastCall context.
type Context struct {
conn net.Conn
req *protocol.Message
ctx *share.Context
async bool
}
// NewContext creates a server.Context for Handler.
func NewContext(ctx *share.Context, conn net.Conn, req *protocol.Message, async bool) *Context {
return &Context{conn: conn, req: req, ctx: ctx, async: async}
}
// Get returns value for key.
func (ctx *Context) Get(key interface{}) interface{} {
return ctx.ctx.Value(key)
}
// SetValue sets the kv pair.
func (ctx *Context) SetValue(key, val interface{}) {
if key == nil || val == nil {
return
}
ctx.ctx.SetValue(key, val)
}
// DeleteKey delete the kv pair by key.
func (ctx *Context) DeleteKey(key interface{}) {
if ctx.ctx == nil || key == nil {
return
}
ctx.ctx.DeleteKey(key)
}
// Payload returns the payload.
func (ctx *Context) Payload() []byte {
return ctx.req.Payload
}
// Metadata returns the metadata.
func (ctx *Context) Metadata() map[string]string {
return ctx.req.Metadata
}
// ServicePath returns the ServicePath.
func (ctx *Context) ServicePath() string {
return ctx.req.ServicePath
}
// ServiceMethod returns the ServiceMethod.
func (ctx *Context) ServiceMethod() string {
return ctx.req.ServiceMethod
}
// Bind parses the body data and stores the result to v.
func (ctx *Context) Bind(v interface{}) error {
req := ctx.req
if v != nil {
codec := share.Codecs[req.SerializeType()]
if codec == nil {
return fmt.Errorf("can not find codec for %d", req.SerializeType())
}
err := codec.Decode(req.Payload, v)
if err != nil {
return err
}
}
return nil
}
func (ctx *Context) Write(v interface{}) error {
req := ctx.req
if req.IsOneway() { // no need to send response
return nil
}
codec := share.Codecs[req.SerializeType()]
if codec == nil {
return fmt.Errorf("can not find codec for %d", req.SerializeType())
}
res := req.Clone()
res.SetMessageType(protocol.Response)
if v != nil {
data, err := codec.Encode(v)
if err != nil {
return err
}
res.Payload = data
}
resMetadata := ctx.Get(share.ResMetaDataKey)
if resMetadata != nil {
resMetaInCtx := resMetadata.(map[string]string)
meta := res.Metadata
if meta == nil {
res.Metadata = resMetaInCtx
} else {
for k, v := range resMetaInCtx {
if meta[k] == "" {
meta[k] = v
}
}
}
}
if len(res.Payload) > 1024 && req.CompressType() != protocol.None {
res.SetCompressType(req.CompressType())
}
respData := res.EncodeSlicePointer()
var err error
if ctx.async {
go func() {
_, err = ctx.conn.Write(*respData)
protocol.PutData(respData)
}()
} else {
_, err = ctx.conn.Write(*respData)
protocol.PutData(respData)
}
return err
}
func (ctx *Context) WriteError(err error) error {
req := ctx.req
if req.IsOneway() { // no need to send response
return nil
}
codec := share.Codecs[req.SerializeType()]
if codec == nil {
return fmt.Errorf("can not find codec for %d", req.SerializeType())
}
res := req.Clone()
res.SetMessageType(protocol.Response)
resMetadata := ctx.Get(share.ResMetaDataKey)
if resMetadata != nil {
resMetaInCtx := resMetadata.(map[string]string)
meta := res.Metadata
if meta == nil {
res.Metadata = resMetaInCtx
} else {
for k, v := range resMetaInCtx {
if meta[k] == "" {
meta[k] = v
}
}
}
}
res.SetMessageStatusType(protocol.Error)
res.Metadata[protocol.ServiceError] = err.Error()
respData := res.EncodeSlicePointer()
ctx.conn.Write(*respData)
protocol.PutData(respData)
return nil
}
================================================
FILE: server/converter.go
================================================
package server
import (
"io"
"net/http"
"net/url"
"strconv"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
)
const (
XVersion = "X-RPCX-Version"
XMessageType = "X-RPCX-MessageType"
XHeartbeat = "X-RPCX-Heartbeat"
XOneway = "X-RPCX-Oneway"
XMessageStatusType = "X-RPCX-MessageStatusType"
XSerializeType = "X-RPCX-SerializeType"
XCompressType = "X-RPCX-CompressType"
XMessageID = "X-RPCX-MessageID"
XServicePath = "X-RPCX-ServicePath"
XServiceMethod = "X-RPCX-ServiceMethod"
XMeta = "X-RPCX-Meta"
XErrorMessage = "X-RPCX-ErrorMessage"
)
// HTTPRequest2RpcxRequest converts a http request to a rpcx request.
func HTTPRequest2RpcxRequest(r *http.Request) (*protocol.Message, error) {
req := protocol.NewMessage()
req.SetMessageType(protocol.Request)
h := r.Header
seq := h.Get(XMessageID)
if seq != "" {
id, err := strconv.ParseUint(seq, 10, 64)
if err != nil {
return nil, err
}
req.SetSeq(id)
}
heartbeat := h.Get(XHeartbeat)
if heartbeat != "" {
req.SetHeartbeat(true)
}
oneway := h.Get(XOneway)
if oneway != "" {
req.SetOneway(true)
}
st := h.Get(XSerializeType)
if st != "" {
rst, err := strconv.Atoi(st)
if err != nil {
return nil, err
}
req.SetSerializeType(protocol.SerializeType(rst))
}
compressType := h.Get(XCompressType)
if compressType != "" {
ct, err := strconv.Atoi(compressType)
if err != nil {
return nil, err
}
req.SetCompressType(protocol.CompressType(ct))
}
meta := h.Get(XMeta)
if meta != "" {
metadata, err := url.ParseQuery(meta)
if err != nil {
return nil, err
}
mm := make(map[string]string)
for k, v := range metadata {
if len(v) > 0 {
mm[k] = v[0]
}
}
req.Metadata = mm
}
auth := h.Get("Authorization")
if auth != "" {
if req.Metadata == nil {
req.Metadata = make(map[string]string)
}
req.Metadata[share.AuthKey] = auth
}
req.ServicePath = h.Get(XServicePath)
req.ServiceMethod = h.Get(XServiceMethod)
payload, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
req.Payload = payload
return req, nil
}
// func RpcxResponse2HttpResponse(res *protocol.Message) (url.Values, []byte, error) {
// m := make(url.Values)
// m.Set(XVersion, strconv.Itoa(int(res.Version())))
// if res.IsHeartbeat() {
// m.Set(XHeartbeat, "true")
// }
// if res.IsOneway() {
// m.Set(XOneway, "true")
// }
// if res.MessageStatusType() == protocol.Error {
// m.Set(XMessageStatusType, "Error")
// } else {
// m.Set(XMessageStatusType, "Normal")
// }
// if res.CompressType() == protocol.Gzip {
// m.Set("Content-Encoding", "gzip")
// }
// m.Set(XSerializeType, strconv.Itoa(int(res.SerializeType())))
// m.Set(XMessageID, strconv.FormatUint(res.Seq(), 10))
// m.Set(XServicePath, res.ServicePath)
// m.Set(XServiceMethod, res.ServiceMethod)
// return m, res.Payload, nil
// }
================================================
FILE: server/converter_test.go
================================================
package server
import (
"bytes"
"net/http"
"testing"
"github.com/smallnest/rpcx/codec"
"github.com/smallnest/rpcx/share"
"github.com/stretchr/testify/assert"
)
func TestHTTPRequest2RpcxRequest(t *testing.T) {
cc := &codec.MsgpackCodec{}
args := &Args{
A: 10,
B: 20,
}
data, _ := cc.Encode(args)
req, err := http.NewRequest("POST", "http://127.0.0.1:8972/", bytes.NewReader(data))
if err != nil {
t.Fatal("failed to create request: ", err)
return
}
h := req.Header
h.Set(XMessageID, "10000")
h.Set(XHeartbeat, "0")
h.Set(XOneway, "0")
h.Set(XSerializeType, "3")
h.Set(XMeta, "Meta")
h.Set("Authorization", "Authorization")
h.Set(XServicePath, "ProxyServer")
h.Set(XServiceMethod, "GetAdData")
rpcxReq, err := HTTPRequest2RpcxRequest(req)
if err != nil {
t.Fatal("HTTPRequest2RpcxRequest() error")
}
assert.NotNil(t, rpcxReq.Metadata)
assert.Equal(t, h.Get("Authorization"), rpcxReq.Metadata[share.AuthKey])
assert.Equal(t, h.Get(XServicePath), rpcxReq.ServicePath)
assert.Equal(t, h.Get(XServiceMethod), rpcxReq.ServiceMethod)
}
================================================
FILE: server/file_transfer.go
================================================
package server
import (
"context"
"crypto/rand"
"io"
"net"
"sync"
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/share"
)
// FileTransferHandler handles uploading file. Must close the connection after it finished.
type FileTransferHandler func(conn net.Conn, args *share.FileTransferArgs)
// DownloadFileHandler handles downloading file. Must close the connection after it finished.
type DownloadFileHandler func(conn net.Conn, args *share.DownloadFileArgs)
type tokenInfo struct {
token []byte
args *share.FileTransferArgs
}
type downloadTokenInfo struct {
token []byte
args *share.DownloadFileArgs
}
// FileTransfer support transfer files from clients.
// It registers a file transfer service and listens a on the given port.
// Clients will invokes this service to get the token and send the token and the file to this port.
type FileTransfer struct {
Addr string
AdvertiseAddr string
handler FileTransferHandler
downloadFileHandler DownloadFileHandler
cachedTokens *lru.Cache
service *FileTransferService
startOnce sync.Once
ln net.Listener
done chan struct{}
}
type FileTransferService struct {
FileTransfer *FileTransfer
}
// NewFileTransfer creates a FileTransfer with given parameters.
func NewFileTransfer(addr string, handler FileTransferHandler, downloadFileHandler DownloadFileHandler, waitNum int) *FileTransfer {
cachedTokens, _ := lru.New(waitNum)
fi := &FileTransfer{
Addr: addr,
handler: handler,
downloadFileHandler: downloadFileHandler,
cachedTokens: cachedTokens,
}
fi.service = &FileTransferService{
FileTransfer: fi,
}
return fi
}
// EnableFileTransfer supports filetransfer service in this server.
func (s *Server) EnableFileTransfer(serviceName string, fileTransfer *FileTransfer) {
if serviceName == "" {
serviceName = share.SendFileServiceName
}
_ = fileTransfer.Start()
_ = s.RegisterName(serviceName, fileTransfer.service, "")
}
func (s *FileTransferService) TransferFile(ctx context.Context, args *share.FileTransferArgs, reply *share.FileTransferReply) error {
token := make([]byte, 32)
_, err := rand.Read(token)
if err != nil {
return err
}
*reply = share.FileTransferReply{
Token: token,
Addr: s.FileTransfer.Addr,
}
if s.FileTransfer.AdvertiseAddr != "" {
reply.Addr = s.FileTransfer.AdvertiseAddr
}
cloned := args.Clone()
s.FileTransfer.cachedTokens.Add(string(token), &tokenInfo{token, cloned})
return nil
}
func (s *FileTransferService) DownloadFile(ctx context.Context, args *share.DownloadFileArgs, reply *share.FileTransferReply) error {
token := make([]byte, 32)
_, err := rand.Read(token)
if err != nil {
return err
}
*reply = share.FileTransferReply{
Token: token,
Addr: s.FileTransfer.Addr,
}
if s.FileTransfer.AdvertiseAddr != "" {
reply.Addr = s.FileTransfer.AdvertiseAddr
}
cloned := args.Clone()
s.FileTransfer.cachedTokens.Add(string(token), &downloadTokenInfo{token, cloned})
return nil
}
func (s *FileTransfer) Start() error {
s.startOnce.Do(func() {
go s.start()
})
return nil
}
func (s *FileTransfer) start() error {
ln, err := net.Listen("tcp", s.Addr)
if err != nil {
return err
}
s.ln = ln
var tempDelay time.Duration
for {
select {
case <-s.done:
return nil
default:
conn, e := ln.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
log.Errorf("filetransfer: accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
if tc, ok := conn.(*net.TCPConn); ok {
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
tc.SetLinger(10)
}
token := make([]byte, 32)
_, err := io.ReadFull(conn, token)
if err != nil {
conn.Close()
log.Errorf("failed to read token from %s", conn.RemoteAddr().String())
continue
}
tokenStr := string(token)
info, ok := s.cachedTokens.Get(tokenStr)
if !ok {
conn.Close()
log.Errorf("failed to read token from %s", conn.RemoteAddr().String())
continue
}
s.cachedTokens.Remove(tokenStr)
switch ti := info.(type) {
case *tokenInfo:
if s.handler == nil {
conn.Close()
continue
}
go s.handler(conn, ti.args)
case *downloadTokenInfo:
if s.downloadFileHandler == nil {
conn.Close()
continue
}
go s.downloadFileHandler(conn, ti.args)
default:
conn.Close()
}
}
}
}
func (s *FileTransfer) Stop() error {
close(s.done)
// notify Accept() to return
if s.ln != nil {
s.ln.Close()
}
return nil
}
================================================
FILE: server/gateway.go
================================================
package server
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/julienschmidt/httprouter"
"github.com/rs/cors"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
"github.com/soheilhy/cmux"
)
func (s *Server) startGateway(network string, ln net.Listener) net.Listener {
if network != "tcp" && network != "tcp4" && network != "tcp6" && network != "reuseport" {
// log.Infof("network is not tcp/tcp4/tcp6 so can not start gateway")
return ln
}
m := cmux.New(ln)
rpcxLn := m.Match(rpcxPrefixByteMatcher())
// mux Plugins
if s.Plugins != nil {
s.Plugins.MuxMatch(m)
}
if !s.DisableJSONRPC {
jsonrpc2Ln := m.Match(cmux.HTTP1HeaderField("X-JSONRPC-2.0", "true"))
go s.startJSONRPC2(jsonrpc2Ln)
}
if !s.DisableHTTPGateway {
httpLn := m.Match(cmux.HTTP1Fast()) // X-RPCX-MessageID
go s.startHTTP1APIGateway(httpLn)
}
go m.Serve()
return rpcxLn
}
func http1Path(prefix string) cmux.Matcher {
return func(r io.Reader) bool {
return matchHTTP1Field(r, prefix, func(gotValue string) bool {
br := bufio.NewReader(&io.LimitedReader{R: r, N: 1024})
l, part, err := br.ReadLine()
if err != nil || part {
return false
}
_, uri, _, ok := parseRequestLine(string(l))
if !ok {
return false
}
if strings.HasPrefix(uri, prefix) {
return true
}
u, err := url.Parse(uri)
if err != nil {
return false
}
return strings.HasPrefix(u.Path, prefix)
})
}
}
// grabbed from net/http.
func parseRequestLine(line string) (method, uri, proto string, ok bool) {
s1 := strings.Index(line, " ")
s2 := strings.Index(line[s1+1:], " ")
if s1 < 0 || s2 < 0 {
return
}
s2 += s1 + 1
return line[:s1], line[s1+1 : s2], line[s2+1:], true
}
func http1HeaderExist(name string) cmux.Matcher {
return func(r io.Reader) bool {
return matchHTTP1Field(r, name, func(gotValue string) bool {
req, err := http.ReadRequest(bufio.NewReader(r))
if err != nil {
return false
}
return req.Header.Get(name) != ""
})
}
}
func matchHTTP1Field(r io.Reader, name string, matches func(string) bool) (matched bool) {
req, err := http.ReadRequest(bufio.NewReader(r))
if err != nil {
return false
}
return matches(req.Header.Get(name))
}
func rpcxPrefixByteMatcher() cmux.Matcher {
magic := protocol.MagicNumber()
return func(r io.Reader) bool {
buf := make([]byte, 1)
n, _ := r.Read(buf)
return n == 1 && buf[0] == magic
}
}
func (s *Server) startHTTP1APIGateway(ln net.Listener) {
router := httprouter.New()
router.POST("/*servicePath", s.handleGatewayRequest)
router.GET("/*servicePath", s.handleGatewayRequest)
router.PUT("/*servicePath", s.handleGatewayRequest)
if s.corsOptions != nil {
opt := cors.Options(*s.corsOptions)
c := cors.New(opt)
mux := c.Handler(router)
s.mu.Lock()
s.gatewayHTTPServer = &http.Server{Handler: mux}
s.mu.Unlock()
} else {
s.mu.Lock()
s.gatewayHTTPServer = &http.Server{Handler: router}
s.mu.Unlock()
}
if err := s.gatewayHTTPServer.Serve(ln); err != nil {
if errors.Is(err, ErrServerClosed) || errors.Is(err, cmux.ErrListenerClosed) || errors.Is(err, cmux.ErrServerClosed) {
log.Info("gateway server closed")
} else {
log.Warnf("error in gateway Serve: %T %s", err, err)
}
}
}
func (s *Server) closeHTTP1APIGateway(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.gatewayHTTPServer != nil {
return s.gatewayHTTPServer.Shutdown(ctx)
}
return nil
}
func (s *Server) handleGatewayRequest(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
ctx := share.WithValue(r.Context(), RemoteConnContextKey, r.RemoteAddr) // notice: It is a string, different with TCP (net.Conn)
err := s.Plugins.DoPreReadRequest(ctx)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = s.Plugins.DoPostHTTPRequest(ctx, r, params)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if r.Header.Get(XServicePath) == "" {
servicePath := params.ByName("servicePath")
servicePath = strings.TrimPrefix(servicePath, "/")
r.Header.Set(XServicePath, servicePath)
}
servicePath := r.Header.Get(XServicePath)
wh := w.Header()
req, err := HTTPRequest2RpcxRequest(r)
// set headers
wh.Set(XVersion, r.Header.Get(XVersion))
wh.Set(XMessageID, r.Header.Get(XMessageID))
if err == nil && servicePath == "" {
err = errors.New("empty servicepath")
} else {
wh.Set(XServicePath, servicePath)
}
if err == nil && r.Header.Get(XServiceMethod) == "" {
err = errors.New("empty servicemethod")
} else {
wh.Set(XServiceMethod, r.Header.Get(XServiceMethod))
}
if err == nil && r.Header.Get(XSerializeType) == "" {
err = errors.New("empty serialized type")
} else {
wh.Set(XSerializeType, r.Header.Get(XSerializeType))
}
if err != nil {
rh := r.Header
for k, v := range rh {
if strings.HasPrefix(k, "X-RPCX-") && len(v) > 0 {
wh.Set(k, v[0])
}
}
wh.Set(XMessageStatusType, "Error")
wh.Set(XErrorMessage, err.Error())
return
}
err = s.Plugins.DoPostReadRequest(ctx, req, nil)
if err != nil {
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
http.Error(w, err.Error(), 500)
s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err)
return
}
ctx.SetValue(StartRequestContextKey, time.Now().UnixNano())
err = s.auth(ctx, req)
if err != nil {
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
wh.Set(XMessageStatusType, "Error")
wh.Set(XErrorMessage, err.Error())
w.WriteHeader(401)
s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err)
return
}
resMetadata := make(map[string]string)
newCtx := share.WithLocalValue(share.WithLocalValue(ctx, share.ReqMetaDataKey, req.Metadata),
share.ResMetaDataKey, resMetadata)
res, err := s.handleRequest(newCtx, req)
if err != nil {
// call DoPreWriteResponse
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
if s.HandleServiceError != nil {
s.HandleServiceError(err)
} else {
log.Warnf("rpcx: gateway request: %v", err)
}
wh.Set(XMessageStatusType, "Error")
wh.Set(XErrorMessage, err.Error())
w.WriteHeader(500)
// call DoPostWriteResponse
s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err)
return
}
// will set res to call
s.Plugins.DoPreWriteResponse(newCtx, req, res, nil)
if len(resMetadata) > 0 { // copy meta in context to request
meta := res.Metadata
if meta == nil {
res.Metadata = resMetadata
} else {
for k, v := range resMetadata {
meta[k] = v
}
}
}
meta := url.Values{}
for k, v := range res.Metadata {
meta.Add(k, v)
}
wh.Set(XMeta, meta.Encode())
if res.CompressType() != protocol.None {
wh.Set(XCompressType, fmt.Sprintf("%d", res.CompressType()))
}
w.Write(res.Payload)
s.Plugins.DoPostWriteResponse(newCtx, req, res, err)
}
================================================
FILE: server/jsonrpc2.go
================================================
package server
import (
"context"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"net/url"
"strings"
"github.com/rs/cors"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
"github.com/soheilhy/cmux"
)
func (s *Server) jsonrpcHandler(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var req = &jsonrpcRequest{}
err = json.Unmarshal(data, req)
if err != nil {
var res = &jsonrpcRespone{}
res.Error = &JSONRPCError{
Code: CodeParseJSONRPCError,
Message: err.Error(),
}
writeResponse(w, res)
return
}
conn := r.Context().Value(HttpConnContextKey).(net.Conn)
ctx := share.WithValue(r.Context(), RemoteConnContextKey, conn)
if req.ID != nil {
res := s.handleJSONRPCRequest(ctx, req, r.Header)
writeResponse(w, res)
return
}
// notification
go s.handleJSONRPCRequest(ctx, req, r.Header)
}
func (s *Server) handleJSONRPCRequest(ctx context.Context, r *jsonrpcRequest, header http.Header) *jsonrpcRespone {
s.Plugins.DoPreReadRequest(ctx)
var res = &jsonrpcRespone{}
res.ID = r.ID
req := protocol.NewMessage()
if req.Metadata == nil {
req.Metadata = make(map[string]string)
}
if r.ID == nil {
req.SetOneway(true)
}
req.SetMessageType(protocol.Request)
req.SetSerializeType(protocol.JSON)
lastDot := strings.LastIndex(r.Method, ".")
if lastDot <= 0 {
res.Error = &JSONRPCError{
Code: CodeMethodNotFound,
Message: "must contains servicepath and method",
}
return res
}
req.ServicePath = r.Method[:lastDot]
req.ServiceMethod = r.Method[lastDot+1:]
req.Payload = *r.Params
// meta
meta := header.Get(XMeta)
if meta != "" {
metadata, _ := url.ParseQuery(meta)
for k, v := range metadata {
if len(v) > 0 {
req.Metadata[k] = v[0]
}
}
}
auth := header.Get("Authorization")
if auth != "" {
req.Metadata[share.AuthKey] = auth
}
err := s.Plugins.DoPostReadRequest(ctx, req, nil)
if err != nil {
res.Error = &JSONRPCError{
Code: CodeInternalJSONRPCError,
Message: err.Error(),
}
return res
}
err = s.auth(ctx, req)
if err != nil {
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
res.Error = &JSONRPCError{
Code: CodeInternalJSONRPCError,
Message: err.Error(),
}
s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err)
return res
}
resp, err := s.handleRequest(ctx, req)
if r.ID == nil {
return nil
}
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
if err != nil {
res.Error = &JSONRPCError{
Code: CodeInternalJSONRPCError,
Message: err.Error(),
}
s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err)
return res
}
result := json.RawMessage(resp.Payload)
res.Result = &result
s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err)
return res
}
func writeResponse(w http.ResponseWriter, res *jsonrpcRespone) {
data, err := json.Marshal(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Context-Type", "application/json")
w.Write(data)
}
type CORSOptions = cors.Options
// AllowAllCORSOptions returns a option that allows access.
func AllowAllCORSOptions() *CORSOptions {
return &CORSOptions{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
}
}
// SetCORS sets CORS options.
// for example:
//
// cors.Options{
// AllowedOrigins: []string{"foo.com"},
// AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete},
// AllowCredentials: true,
// }
func (s *Server) SetCORS(options *CORSOptions) {
s.corsOptions = options
}
func (s *Server) startJSONRPC2(ln net.Listener) {
newServer := http.NewServeMux()
newServer.HandleFunc("/", s.jsonrpcHandler)
srv := http.Server{ConnContext: func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, HttpConnContextKey, c)
}}
if s.corsOptions != nil {
opt := cors.Options(*s.corsOptions)
c := cors.New(opt)
mux := c.Handler(newServer)
srv.Handler = mux
} else {
srv.Handler = newServer
}
s.jsonrpcHTTPServerLock.Lock()
s.jsonrpcHTTPServer = &srv
s.jsonrpcHTTPServerLock.Unlock()
if err := s.jsonrpcHTTPServer.Serve(ln); !errors.Is(err, cmux.ErrServerClosed) {
log.Errorf("error in JSONRPC server: %T %s", err, err)
}
}
func (s *Server) closeJSONRPC2(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.jsonrpcHTTPServer != nil {
return s.jsonrpcHTTPServer.Shutdown(ctx)
}
return nil
}
================================================
FILE: server/jsonrpc2_wire.go
================================================
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package server
import (
"encoding/json"
"fmt"
"strconv"
)
// this file contains the go forms of the wire specification
// see http://www.jsonrpc.org/specification for details
const (
// CodeUnknownJSONRPCError should be used for all non coded errors.
CodeUnknownJSONRPCError = -32001
// CodeParseJSONRPCError is used when invalid JSON was received by the server.
CodeParseJSONRPCError = -32700
// CodeInvalidjsonrpcRequest is used when the JSON sent is not a valid jsonrpcRequest object.
CodeInvalidjsonrpcRequest = -32600
// CodeMethodNotFound should be returned by the handler when the method does
// not exist / is not available.
CodeMethodNotFound = -32601
// CodeInvalidParams should be returned by the handler when method
// parameter(s) were invalid.
CodeInvalidParams = -32602
// CodeInternalJSONRPCError is not currently returned but defined for completeness.
CodeInternalJSONRPCError = -32603
)
// jsonrpcRequest is sent to a server to represent a Call or Notify operation.
type jsonrpcRequest struct {
// VersionTag is always encoded as the string "2.0"
VersionTag VersionTag `json:"jsonrpc"`
// Method is a string containing the method name to invoke.
Method string `json:"method"`
// Params is either a struct or an array with the parameters of the method.
Params *json.RawMessage `json:"params,omitempty"`
// The id of this request, used to tie the jsonrpcRespone back to the request.
// Will be either a string or a number. If not set, the jsonrpcRequest is a notify,
// and no response is possible.
ID *ID `json:"id,omitempty"`
}
// jsonrpcRespone is a reply to a jsonrpcRequest.
// It will always have the ID field set to tie it back to a request, and will
// have either the Result or JSONRPCError fields set depending on whether it is a
// success or failure response.
type jsonrpcRespone struct {
// VersionTag is always encoded as the string "2.0"
VersionTag VersionTag `json:"jsonrpc"`
// Result is the response value, and is required on success.
Result *json.RawMessage `json:"result,omitempty"`
// Error is a structured error response if the call fails.
Error *JSONRPCError `json:"error,omitempty"`
// ID must be set and is the identifier of the jsonrpcRequest this is a response to.
ID *ID `json:"id,omitempty"`
}
// JSONRPCError represents a structured error in a jsonrpcRespone.
type JSONRPCError struct {
// Code is an error code indicating the type of failure.
Code int64 `json:"code"`
// Message is a short description of the error.
Message string `json:"message"`
// Data is optional structured data containing additional information about the error.
Data *json.RawMessage `json:"data"`
}
// VersionTag is a special 0 sized struct that encodes as the jsonrpc version
// tag.
// It will fail during decode if it is not the correct version tag in the
// stream.
type VersionTag struct{}
// ID is a jsonrpcRequest identifier.
// Only one of either the Name or Number members will be set, using the
// number form if the Name is the empty string.
type ID struct {
Name string
Number int64
}
// IsNotify returns true if this request is a notification.
func (r *jsonrpcRequest) IsNotify() bool {
return r.ID == nil
}
func (err *JSONRPCError) JSONRPCError() string {
if err == nil {
return ""
}
return err.Message
}
func (VersionTag) MarshalJSON() ([]byte, error) {
return json.Marshal("2.0")
}
func (VersionTag) UnmarshalJSON(data []byte) error {
version := ""
if err := json.Unmarshal(data, &version); err != nil {
return err
}
if version != "2.0" {
return fmt.Errorf("invalid RPC version %v", version)
}
return nil
}
// String returns a string representation of the ID.
// The representation is non ambiguous, string forms are quoted, number forms
// are preceded by a #
func (id *ID) String() string {
if id == nil {
return ""
}
if id.Name != "" {
return strconv.Quote(id.Name)
}
return "#" + strconv.FormatInt(id.Number, 10)
}
func (id *ID) MarshalJSON() ([]byte, error) {
if id.Name != "" {
return json.Marshal(id.Name)
}
return json.Marshal(id.Number)
}
func (id *ID) UnmarshalJSON(data []byte) error {
*id = ID{}
if err := json.Unmarshal(data, &id.Number); err == nil {
return nil
}
return json.Unmarshal(data, &id.Name)
}
================================================
FILE: server/kcp.go
================================================
// +build kcp
package server
import (
"errors"
"net"
kcp "github.com/xtaci/kcp-go"
)
func init() {
makeListeners["kcp"] = kcpMakeListener
}
func kcpMakeListener(s *Server, address string) (ln net.Listener, err error) {
if s.options == nil || s.options["BlockCrypt"] == nil {
return nil, errors.New("KCP BlockCrypt must be configured in server.Options")
}
return kcp.ListenWithOptions(address, s.options["BlockCrypt"].(kcp.BlockCrypt), 10, 3)
}
// WithBlockCrypt sets kcp.BlockCrypt.
func WithBlockCrypt(bc kcp.BlockCrypt) OptionFn {
return func(s *Server) {
s.options["BlockCrypt"] = bc
}
}
================================================
FILE: server/listener.go
================================================
package server
import (
"crypto/tls"
"errors"
"fmt"
"net"
)
var makeListeners = make(map[string]MakeListener)
func init() {
makeListeners["tcp"] = tcpMakeListener("tcp")
makeListeners["tcp4"] = tcpMakeListener("tcp4")
makeListeners["tcp6"] = tcpMakeListener("tcp6")
makeListeners["http"] = tcpMakeListener("tcp")
makeListeners["ws"] = tcpMakeListener("tcp")
makeListeners["wss"] = tcpMakeListener("tcp")
}
// RegisterMakeListener registers a MakeListener for network.
func RegisterMakeListener(network string, ml MakeListener) {
makeListeners[network] = ml
}
// MakeListener defines a listener generator.
type MakeListener func(s *Server, address string) (ln net.Listener, err error)
// block can be nil if the caller wishes to skip encryption in kcp.
// tlsConfig can be nil iff we are not using network "quic".
func (s *Server) makeListener(network, address string) (ln net.Listener, err error) {
ml := makeListeners[network]
if ml == nil {
return nil, fmt.Errorf("can not make listener for %s", network)
}
if network == "wss" && s.tlsConfig == nil {
return nil, errors.New("must set tlsconfig for wss")
}
return ml(s, address)
}
func tcpMakeListener(network string) MakeListener {
return func(s *Server, address string) (ln net.Listener, err error) {
if s.tlsConfig == nil {
ln, err = net.Listen(network, address)
} else {
ln, err = tls.Listen(network, address, s.tlsConfig)
}
return ln, err
}
}
================================================
FILE: server/listener_linux.go
================================================
//go:build linux
// +build linux
package server
import (
"net"
"runtime"
"time"
uringnet "github.com/godzie44/go-uring/net"
"github.com/godzie44/go-uring/reactor"
"github.com/godzie44/go-uring/uring"
"github.com/smallnest/rpcx/log"
)
func init() {
makeListeners["iouring"] = iouringMakeListener
}
// iouringMakeListener creates a new listener using io_uring.
// You can use RegisterMakeListener to register a customized iouring Listener creator.
// experimental
func iouringMakeListener(s *Server, address string) (ln net.Listener, err error) {
n := runtime.GOMAXPROCS(-1)
var opts []uring.SetupOption
opts = append(opts, uring.WithSQPoll(time.Millisecond*100))
rings, closeRings, err := uring.CreateMany(n, uring.MaxEntries>>3, n, opts...)
if err != nil {
return nil, err
}
netReactor, err := reactor.NewNet(rings, reactor.WithLogger(&uringLogger{log.GetLogger()}))
if err != nil {
return nil, err
}
ln, err = uringnet.NewListener(net.ListenConfig{}, address, netReactor)
if err != nil {
return nil, err
}
return &uringnetListener{Listener: ln, closeRings: closeRings}, nil
}
type uringnetListener struct {
net.Listener
closeRings uring.Defer
}
func (cl *uringnetListener) Close() error {
cl.closeRings()
cl.Listener.Close()
return nil
}
type uringLogger struct {
log.Logger
}
func (l *uringLogger) Log(keyvals ...interface{}) {
l.Logger.Info(keyvals...)
}
================================================
FILE: server/listener_rdma.go
================================================
//go:build rdma
// +build rdma
package server
import (
"net"
"os"
"strconv"
"github.com/smallnest/rsocket"
)
func init() {
makeListeners["rdma"] = rdmaMakeListener
}
func rdmaMakeListener(s *Server, address string) (ln net.Listener, err error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
p, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
backlog := os.Getenv("RDMA_BACKLOG")
if backlog == "" {
backlog = "128"
}
blog, _ := strconv.Atoi(backlog)
if blog == 0 {
blog = 128
}
return rsocket.NewTCPListener(host, p, blog)
}
================================================
FILE: server/listener_unix.go
================================================
//go:build !windows
// +build !windows
package server
import (
"net"
reuseport "github.com/kavu/go_reuseport"
)
func init() {
makeListeners["reuseport"] = reuseportMakeListener
makeListeners["unix"] = unixMakeListener
}
func reuseportMakeListener(s *Server, address string) (ln net.Listener, err error) {
var network string
if validIP6(address) {
network = "tcp6"
} else {
network = "tcp4"
}
return reuseport.NewReusablePortListener(network, address)
}
func unixMakeListener(s *Server, address string) (ln net.Listener, err error) {
laddr, err := net.ResolveUnixAddr("unix", address)
if err != nil {
return nil, err
}
return net.ListenUnix("unix", laddr)
}
================================================
FILE: server/memconn.go
================================================
package server
import (
"net"
"github.com/akutz/memconn"
)
func init() {
makeListeners["memu"] = memconnMakeListener
}
func memconnMakeListener(s *Server, address string) (ln net.Listener, err error) {
return memconn.Listen("memu", address)
}
================================================
FILE: server/option.go
================================================
package server
import (
"crypto/tls"
"time"
"github.com/alitto/pond"
)
// OptionFn configures options of server.
type OptionFn func(*Server)
// // WithOptions sets multiple options.
// func WithOptions(ops map[string]interface{}) OptionFn {
// return func(s *Server) {
// for k, v := range ops {
// s.options[k] = v
// }
// }
// }
// WithTLSConfig sets tls.Config.
func WithTLSConfig(cfg *tls.Config) OptionFn {
return func(s *Server) {
s.tlsConfig = cfg
}
}
// WithReadTimeout sets readTimeout.
func WithReadTimeout(readTimeout time.Duration) OptionFn {
return func(s *Server) {
s.readTimeout = readTimeout
}
}
// WithWriteTimeout sets writeTimeout.
func WithWriteTimeout(writeTimeout time.Duration) OptionFn {
return func(s *Server) {
s.writeTimeout = writeTimeout
}
}
// WithPool sets goroutine pool.
func WithPool(maxWorkers, maxCapacity int, options ...pond.Option) OptionFn {
return func(s *Server) {
s.pool = pond.New(maxWorkers, maxCapacity, options...)
}
}
// WithCustomPool uses a custom goroutine pool.
func WithCustomPool(pool WorkerPool) OptionFn {
return func(s *Server) {
s.pool = pool
}
}
// WithAsyncWrite sets AsyncWrite to true.
func WithAsyncWrite() OptionFn {
return func(s *Server) {
s.AsyncWrite = true
}
}
================================================
FILE: server/option_test.go
================================================
package server
import (
"crypto/tls"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestOption(t *testing.T) {
server := NewServer()
cert, _ := tls.LoadX509KeyPair("server.pem", "server.key")
config := &tls.Config{Certificates: []tls.Certificate{cert}}
o := WithTLSConfig(config)
o(server)
assert.Equal(t, config, server.tlsConfig)
o = WithReadTimeout(time.Second)
o(server)
assert.Equal(t, time.Second, server.readTimeout)
o = WithWriteTimeout(time.Second)
o(server)
assert.Equal(t, time.Second, server.writeTimeout)
}
================================================
FILE: server/options.go
================================================
package server
import "time"
// WithTCPKeepAlivePeriod sets tcp keepalive period.
func WithTCPKeepAlivePeriod(period time.Duration) OptionFn {
return func(s *Server) {
s.options["TCPKeepAlivePeriod"] = period
}
}
================================================
FILE: server/plugin.go
================================================
package server
import (
"context"
"net"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/smallnest/rpcx/errors"
"github.com/smallnest/rpcx/protocol"
"github.com/soheilhy/cmux"
)
// PluginContainer represents a plugin container that defines all methods to manage plugins.
// And it also defines all extension points.
type PluginContainer interface {
Add(plugin Plugin)
Remove(plugin Plugin)
All() []Plugin
DoRegister(name string, rcvr interface{}, metadata string) error
DoRegisterFunction(serviceName, fname string, fn interface{}, metadata string) error
DoUnregister(name string) error
DoPostConnAccept(net.Conn) (net.Conn, bool)
DoPostConnClose(net.Conn) bool
DoPreReadRequest(ctx context.Context) error
DoPostReadRequest(ctx context.Context, r *protocol.Message, e error) error
DoPostHTTPRequest(ctx context.Context, r *http.Request, params httprouter.Params) error
DoPreHandleRequest(ctx context.Context, req *protocol.Message) error
DoPreCall(ctx context.Context, serviceName, methodName string, args interface{}) (interface{}, error)
DoPostCall(ctx context.Context, serviceName, methodName string, args, reply interface{}, err error) (interface{}, error)
DoPreWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error
DoPostWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error
DoPreWriteRequest(ctx context.Context) error
DoPostWriteRequest(ctx context.Context, r *protocol.Message, e error) error
DoHeartbeatRequest(ctx context.Context, req *protocol.Message) error
MuxMatch(m cmux.CMux)
}
// Plugin is the server plugin interface.
type Plugin interface{}
type (
// RegisterPlugin is .
RegisterPlugin interface {
Register(name string, rcvr interface{}, metadata string) error
Unregister(name string) error
}
// RegisterFunctionPlugin is .
RegisterFunctionPlugin interface {
RegisterFunction(serviceName, fname string, fn interface{}, metadata string) error
}
// PostConnAcceptPlugin represents connection accept plugin.
// if returns false, it means subsequent IPostConnAcceptPlugins should not continue to handle this conn
// and this conn has been closed.
PostConnAcceptPlugin interface {
HandleConnAccept(net.Conn) (net.Conn, bool)
}
// PostConnClosePlugin represents client connection close plugin.
PostConnClosePlugin interface {
HandleConnClose(net.Conn) bool
}
// PreReadRequestPlugin represents .
PreReadRequestPlugin interface {
PreReadRequest(ctx context.Context) error
}
// PostReadRequestPlugin represents .
PostReadRequestPlugin interface {
PostReadRequest(ctx context.Context, r *protocol.Message, e error) error
}
// PostHTTPRequestPlugin represents .
PostHTTPRequestPlugin interface {
PostHTTPRequest(ctx context.Context, r *http.Request, params httprouter.Params) error
}
// PreHandleRequestPlugin represents .
PreHandleRequestPlugin interface {
PreHandleRequest(ctx context.Context, r *protocol.Message) error
}
PreCallPlugin interface {
PreCall(ctx context.Context, serviceName, methodName string, args interface{}) (interface{}, error)
}
PostCallPlugin interface {
PostCall(ctx context.Context, serviceName, methodName string, args, reply interface{}, err error) (interface{}, error)
}
// PreWriteResponsePlugin represents .
PreWriteResponsePlugin interface {
PreWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error
}
// PostWriteResponsePlugin represents .
PostWriteResponsePlugin interface {
PostWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error
}
// PreWriteRequestPlugin represents .
PreWriteRequestPlugin interface {
PreWriteRequest(ctx context.Context) error
}
// PostWriteRequestPlugin represents .
PostWriteRequestPlugin interface {
PostWriteRequest(ctx context.Context, r *protocol.Message, e error) error
}
// HeartbeatPlugin is .
HeartbeatPlugin interface {
HeartbeatRequest(ctx context.Context, req *protocol.Message) error
}
CMuxPlugin interface {
MuxMatch(m cmux.CMux)
}
)
// pluginContainer implements PluginContainer interface.
type pluginContainer struct {
plugins []Plugin
}
// Add adds a plugin.
func (p *pluginContainer) Add(plugin Plugin) {
p.plugins = append(p.plugins, plugin)
}
// Remove removes a plugin by it's name.
func (p *pluginContainer) Remove(plugin Plugin) {
if p.plugins == nil {
return
}
plugins := make([]Plugin, 0, len(p.plugins))
for _, p := range p.plugins {
if p != plugin {
plugins = append(plugins, p)
}
}
p.plugins = plugins
}
func (p *pluginContainer) All() []Plugin {
return p.plugins
}
// DoRegister invokes DoRegister plugin.
func (p *pluginContainer) DoRegister(name string, rcvr interface{}, metadata string) error {
var es []error
for _, rp := range p.plugins {
if plugin, ok := rp.(RegisterPlugin); ok {
err := plugin.Register(name, rcvr, metadata)
if err != nil {
es = append(es, err)
}
}
}
if len(es) > 0 {
return errors.NewMultiError(es)
}
return nil
}
// DoRegisterFunction invokes DoRegisterFunction plugin.
func (p *pluginContainer) DoRegisterFunction(serviceName, fname string, fn interface{}, metadata string) error {
var es []error
for _, rp := range p.plugins {
if plugin, ok := rp.(RegisterFunctionPlugin); ok {
err := plugin.RegisterFunction(serviceName, fname, fn, metadata)
if err != nil {
es = append(es, err)
}
}
}
if len(es) > 0 {
return errors.NewMultiError(es)
}
return nil
}
// DoUnregister invokes RegisterPlugin.
func (p *pluginContainer) DoUnregister(name string) error {
var es []error
for _, rp := range p.plugins {
if plugin, ok := rp.(RegisterPlugin); ok {
err := plugin.Unregister(name)
if err != nil {
es = append(es, err)
}
}
}
if len(es) > 0 {
return errors.NewMultiError(es)
}
return nil
}
// DoPostConnAccept handles accepted conn
func (p *pluginContainer) DoPostConnAccept(conn net.Conn) (net.Conn, bool) {
var flag bool
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostConnAcceptPlugin); ok {
conn, flag = plugin.HandleConnAccept(conn)
if !flag { // interrupt
conn.Close()
return conn, false
}
}
}
return conn, true
}
// DoPostConnClose handles closed conn
func (p *pluginContainer) DoPostConnClose(conn net.Conn) bool {
var flag bool
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostConnClosePlugin); ok {
flag = plugin.HandleConnClose(conn)
if !flag {
return false
}
}
}
return true
}
// DoPreReadRequest invokes PreReadRequest plugin.
func (p *pluginContainer) DoPreReadRequest(ctx context.Context) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PreReadRequestPlugin); ok {
err := plugin.PreReadRequest(ctx)
if err != nil {
return err
}
}
}
return nil
}
// DoPostReadRequest invokes PostReadRequest plugin.
func (p *pluginContainer) DoPostReadRequest(ctx context.Context, r *protocol.Message, e error) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostReadRequestPlugin); ok {
err := plugin.PostReadRequest(ctx, r, e)
if err != nil {
return err
}
}
}
return nil
}
// DoPostHTTPRequest invokes PostHTTPRequest plugin.
func (p *pluginContainer) DoPostHTTPRequest(ctx context.Context, r *http.Request, params httprouter.Params) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostHTTPRequestPlugin); ok {
err := plugin.PostHTTPRequest(ctx, r, params)
if err != nil {
return err
}
}
}
return nil
}
// DoPreHandleRequest invokes PreHandleRequest plugin.
func (p *pluginContainer) DoPreHandleRequest(ctx context.Context, r *protocol.Message) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PreHandleRequestPlugin); ok {
err := plugin.PreHandleRequest(ctx, r)
if err != nil {
return err
}
}
}
return nil
}
// DoPreCall invokes PreCallPlugin plugin.
func (p *pluginContainer) DoPreCall(ctx context.Context, serviceName, methodName string, args interface{}) (interface{}, error) {
var err error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PreCallPlugin); ok {
args, err = plugin.PreCall(ctx, serviceName, methodName, args)
if err != nil {
return args, err
}
}
}
return args, err
}
// DoPostCall invokes PostCallPlugin plugin.
func (p *pluginContainer) DoPostCall(ctx context.Context, serviceName, methodName string, args, reply interface{}, err error) (interface{}, error) {
var e error
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostCallPlugin); ok {
reply, e = plugin.PostCall(ctx, serviceName, methodName, args, reply, err)
if e != nil {
return reply, e
}
}
}
return reply, e
}
// DoPreWriteResponse invokes PreWriteResponse plugin.
func (p *pluginContainer) DoPreWriteResponse(ctx context.Context, req *protocol.Message, res *protocol.Message, err error) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PreWriteResponsePlugin); ok {
err := plugin.PreWriteResponse(ctx, req, res, err)
if err != nil {
return err
}
}
}
return nil
}
// DoPostWriteResponse invokes PostWriteResponse plugin.
func (p *pluginContainer) DoPostWriteResponse(ctx context.Context, req *protocol.Message, resp *protocol.Message, e error) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostWriteResponsePlugin); ok {
err := plugin.PostWriteResponse(ctx, req, resp, e)
if err != nil {
return err
}
}
}
return nil
}
// DoPreWriteRequest invokes PreWriteRequest plugin.
func (p *pluginContainer) DoPreWriteRequest(ctx context.Context) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PreWriteRequestPlugin); ok {
err := plugin.PreWriteRequest(ctx)
if err != nil {
return err
}
}
}
return nil
}
// DoPostWriteRequest invokes PostWriteRequest plugin.
func (p *pluginContainer) DoPostWriteRequest(ctx context.Context, r *protocol.Message, e error) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(PostWriteRequestPlugin); ok {
err := plugin.PostWriteRequest(ctx, r, e)
if err != nil {
return err
}
}
}
return nil
}
// DoHeartbeatRequest invokes HeartbeatRequest plugin.
func (p *pluginContainer) DoHeartbeatRequest(ctx context.Context, r *protocol.Message) error {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(HeartbeatPlugin); ok {
err := plugin.HeartbeatRequest(ctx, r)
if err != nil {
return err
}
}
}
return nil
}
// MuxMatch adds cmux Match.
func (p *pluginContainer) MuxMatch(m cmux.CMux) {
for i := range p.plugins {
if plugin, ok := p.plugins[i].(CMuxPlugin); ok {
plugin.MuxMatch(m)
}
}
}
================================================
FILE: server/plugin_test.go
================================================
package server
import (
"context"
"net"
"sync"
"testing"
"time"
"github.com/smallnest/rpcx/client"
"github.com/smallnest/rpcx/protocol"
)
type HeartbeatHandler struct{}
func (h *HeartbeatHandler) HeartbeatRequest(ctx context.Context, req *protocol.Message) error {
conn := ctx.Value(RemoteConnContextKey).(net.Conn)
println("OnHeartbeat:", conn.RemoteAddr().String())
return nil
}
// TestPluginHeartbeat: go test -v -test.run TestPluginHeartbeat
func TestPluginHeartbeat(t *testing.T) {
h := &HeartbeatHandler{}
s := NewServer(
WithReadTimeout(time.Duration(5)*time.Second),
WithWriteTimeout(time.Duration(5)*time.Second),
)
s.Plugins.Add(h)
s.RegisterName("Arith", new(Arith), "")
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
// server
defer wg.Done()
err := s.Serve("tcp", "127.0.0.1:9001")
if err != nil {
t.Log(err.Error())
}
}()
go func() {
// wait for server start complete
time.Sleep(time.Second)
defer wg.Done()
// client
opts := client.DefaultOption
opts.Heartbeat = true
opts.HeartbeatInterval = time.Second
opts.IdleTimeout = time.Duration(5) * time.Second
opts.ConnectTimeout = time.Duration(5) * time.Second
// PeerDiscovery
d, err := client.NewPeer2PeerDiscovery("tcp@127.0.0.1:9001", "")
if err != nil {
t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err)
}
c := client.NewXClient("Arith", client.Failtry, client.RoundRobin, d, opts)
i := 0
for {
i++
resp := &Reply{}
c.Call(context.Background(), "Mul", &Args{A: 1, B: 5}, resp)
t.Log("call Mul resp:", resp.C)
time.Sleep(time.Second)
if i > 10 {
break
}
}
c.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s.Shutdown(ctx)
}()
wg.Wait()
}
================================================
FILE: server/pool.go
================================================
package server
import (
"reflect"
"sync"
)
// Reset defines Reset method for pooled object.
type Reset interface {
Reset()
}
var reflectTypePools = &typePools{
pools: make(map[reflect.Type]*sync.Pool),
New: func(t reflect.Type) interface{} {
var argv reflect.Value
if t.Kind() == reflect.Ptr { // reply must be ptr
argv = reflect.New(t.Elem())
} else {
argv = reflect.New(t)
}
return argv.Interface()
},
}
type typePools struct {
mu sync.RWMutex
pools map[reflect.Type]*sync.Pool
New func(t reflect.Type) interface{}
}
func (p *typePools) Init(t reflect.Type) {
tp := &sync.Pool{}
tp.New = func() interface{} {
return p.New(t)
}
p.mu.Lock()
defer p.mu.Unlock()
p.pools[t] = tp
}
func (p *typePools) Put(t reflect.Type, x interface{}) {
if o, ok := x.(Reset); ok {
o.Reset()
p.mu.RLock()
pool := p.pools[t]
p.mu.RUnlock()
pool.Put(x)
}
}
func (p *typePools) Get(t reflect.Type) interface{} {
p.mu.RLock()
pool := p.pools[t]
p.mu.RUnlock()
return pool.Get()
}
================================================
FILE: server/pool_test.go
================================================
package server
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
type Elem struct {
// magicNumber the Magic Number
magicNumber int
}
func (e Elem) Reset() {
}
func TestPool(t *testing.T) {
elem := Elem{42}
elemType := reflect.TypeOf(elem)
// init Elem pool
reflectTypePools.Init(elemType)
reflectTypePools.Put(elemType, elem)
// Get() will remove element from pool
assert.Equal(t, elem.magicNumber, reflectTypePools.Get(elemType).(Elem).magicNumber)
}
================================================
FILE: server/quic.go
================================================
//go:build quic
// +build quic
package server
import (
"errors"
"net"
"github.com/smallnest/quick"
)
func init() {
makeListeners["quic"] = quicMakeListener
}
func quicMakeListener(s *Server, address string) (ln net.Listener, err error) {
if s.tlsConfig == nil {
return nil, errors.New("TLSConfig must be configured in server.Options")
}
if len(s.tlsConfig.NextProtos) == 0 {
s.tlsConfig.NextProtos = []string{"rpcx"}
}
return quick.Listen("udp", address, s.tlsConfig, nil)
}
================================================
FILE: server/router.go
================================================
package server
// UpdateHandler 批量更新router
// 服务器使用plugin热更时,批量替换特定接口
func (s *Server) UpdateHandler(router map[string]Handler) {
newRouter := make(map[string]Handler)
for k, v := range s.router {
newRouter[k] = v
}
for k, v := range router {
newRouter[k] = v
}
s.router = newRouter
}
================================================
FILE: server/server.go
================================================
package server
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
"github.com/soheilhy/cmux"
"golang.org/x/net/websocket"
)
// ErrServerClosed is returned by the Server's Serve, ListenAndServe after a call to Shutdown or Close.
var (
ErrServerClosed = errors.New("http: Server closed")
ErrReqReachLimit = errors.New("request reached rate limit")
)
const (
// ReaderBuffsize is used for bufio reader.
ReaderBuffsize = 1024
// WriterBuffsize is used for bufio writer.
WriterBuffsize = 1024
// // WriteChanSize is used for response.
// WriteChanSize = 1024 * 1024
)
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation.
type contextKey struct {
name string
}
func (k *contextKey) String() string { return "rpcx context value " + k.name }
var (
// RemoteConnContextKey is a context key. It can be used in
// services with context.WithValue to access the connection arrived on.
// The associated value will be of type net.Conn.
RemoteConnContextKey = &contextKey{"remote-conn"}
// StartRequestContextKey records the start time
StartRequestContextKey = &contextKey{"start-parse-request"}
// StartSendRequestContextKey records the start time
StartSendRequestContextKey = &contextKey{"start-send-request"}
// TagContextKey is used to record extra info in handling services. Its value is a map[string]interface{}
TagContextKey = &contextKey{"service-tag"}
// HttpConnContextKey is used to store http connection.
HttpConnContextKey = &contextKey{"http-conn"}
)
type Handler func(ctx *Context) error
type WorkerPool interface {
Submit(task func())
StopAndWaitFor(deadline time.Duration)
Stop() context.Context
StopAndWait()
}
// Server is rpcx server that use TCP or UDP.
type Server struct {
ln net.Listener
readTimeout time.Duration
writeTimeout time.Duration
gatewayHTTPServer *http.Server
jsonrpcHTTPServerLock sync.Mutex
jsonrpcHTTPServer *http.Server
DisableHTTPGateway bool // disable http invoke or not.
DisableJSONRPC bool // disable json rpc or not.
AsyncWrite bool // set true if your server only serves few clients
pool WorkerPool
serviceMapMu sync.RWMutex
serviceMap map[string]*service
router map[string]Handler
mu sync.RWMutex
activeConn map[net.Conn]struct{}
doneChan chan struct{}
seq atomic.Uint64
inShutdown int32
onShutdown []func(s *Server)
onRestart []func(s *Server)
// TLSConfig for creating tls tcp connection.
tlsConfig *tls.Config
// BlockCrypt for kcp.BlockCrypt
options map[string]interface{}
// CORS options
corsOptions *CORSOptions
Plugins PluginContainer
// AuthFunc can be used to auth.
AuthFunc func(ctx context.Context, req *protocol.Message, token string) error
handlerMsgNum int32
requestCount atomic.Uint64
// HandleServiceError is used to get all service errors. You can use it write logs or others.
HandleServiceError func(error)
// ServerErrorFunc is a customized error handlers and you can use it to return customized error strings to clients.
// If not set, it use err.Error()
ServerErrorFunc func(res *protocol.Message, err error) string
// The server is started.
Started chan struct{}
unregisterAllOnce sync.Once
}
// NewServer returns a server.
func NewServer(options ...OptionFn) *Server {
s := &Server{
Plugins: &pluginContainer{},
options: make(map[string]interface{}),
activeConn: make(map[net.Conn]struct{}),
doneChan: make(chan struct{}),
serviceMap: make(map[string]*service),
router: make(map[string]Handler),
AsyncWrite: false, // 除非你想做进一步的优化测试,否则建议你设置为false
Started: make(chan struct{}),
}
for _, op := range options {
op(s)
}
if s.options["TCPKeepAlivePeriod"] == nil {
s.options["TCPKeepAlivePeriod"] = 3 * time.Minute
}
return s
}
// Address returns listened address.
func (s *Server) Address() net.Addr {
s.mu.RLock()
defer s.mu.RUnlock()
if s.ln == nil {
return nil
}
return s.ln.Addr()
}
func (s *Server) AddHandler(servicePath, serviceMethod string, handler func(*Context) error) {
s.router[servicePath+"."+serviceMethod] = handler
}
// ActiveClientConn returns active connections.
func (s *Server) ActiveClientConn() []net.Conn {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]net.Conn, 0, len(s.activeConn))
for clientConn := range s.activeConn {
result = append(result, clientConn)
}
return result
}
// SendMessage a request to the specified client.
// The client is designated by the conn.
// conn can be gotten from context in services:
//
// ctx.Value(RemoteConnContextKey)
//
// servicePath, serviceMethod, metadata can be set to zero values.
func (s *Server) SendMessage(conn net.Conn, servicePath, serviceMethod string, metadata map[string]string, data []byte) error {
ctx := share.WithValue(context.Background(), StartSendRequestContextKey, time.Now().UnixNano())
s.Plugins.DoPreWriteRequest(ctx)
req := protocol.NewMessage()
req.SetMessageType(protocol.Request)
seq := s.seq.Add(1)
req.SetSeq(seq)
req.SetOneway(true)
req.SetSerializeType(protocol.SerializeNone)
req.ServicePath = servicePath
req.ServiceMethod = serviceMethod
req.Metadata = metadata
req.Payload = data
b := req.EncodeSlicePointer()
_, err := conn.Write(*b)
protocol.PutData(b)
s.Plugins.DoPostWriteRequest(ctx, req, err)
return err
}
func (s *Server) getDoneChan() <-chan struct{} {
return s.doneChan
}
// Serve starts and listens RPC requests.
// It is blocked until receiving connections from clients.
func (s *Server) Serve(network, address string) (err error) {
var ln net.Listener
ln, err = s.makeListener(network, address)
if err != nil {
return err
}
defer s.UnregisterAll()
if network == "http" {
s.serveByHTTP(ln, "")
return nil
}
if network == "ws" || network == "wss" {
s.serveByWS(ln, "")
return nil
}
// try to start gateway
ln = s.startGateway(network, ln)
return s.serveListener(ln)
}
// ServeListener listens RPC requests.
// It is blocked until receiving connections from clients.
func (s *Server) ServeListener(network string, ln net.Listener) (err error) {
defer s.UnregisterAll()
if network == "http" {
s.serveByHTTP(ln, "")
return nil
}
// try to start gateway
ln = s.startGateway(network, ln)
return s.serveListener(ln)
}
// serveListener accepts incoming connections on the Listener ln,
// creating a new service goroutine for each.
// The service goroutines read requests and then call services to reply to them.
func (s *Server) serveListener(ln net.Listener) error {
var tempDelay time.Duration
s.mu.Lock()
s.ln = ln
close(s.Started)
s.mu.Unlock()
for {
conn, e := ln.Accept()
if e != nil {
if s.isShutdown() {
<-s.doneChan
return ErrServerClosed
}
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
log.Errorf("rpcx: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
if errors.Is(e, cmux.ErrListenerClosed) {
return ErrServerClosed
}
return e
}
tempDelay = 0
if tc, ok := conn.(*net.TCPConn); ok {
period := s.options["TCPKeepAlivePeriod"]
if period != nil {
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(period.(time.Duration))
tc.SetLinger(10)
}
}
conn, ok := s.Plugins.DoPostConnAccept(conn)
if !ok {
conn.Close()
continue
}
s.mu.Lock()
s.activeConn[conn] = struct{}{}
s.mu.Unlock()
if share.Trace {
log.Debugf("server accepted an conn: %v", conn.RemoteAddr().String())
}
go s.serveConn(conn)
}
}
// serveByHTTP serves by HTTP.
// if rpcPath is an empty string, use share.DefaultRPCPath.
func (s *Server) serveByHTTP(ln net.Listener, rpcPath string) {
s.ln = ln
if rpcPath == "" {
rpcPath = share.DefaultRPCPath
}
mux := http.NewServeMux()
mux.Handle(rpcPath, s)
srv := &http.Server{Handler: mux}
srv.Serve(ln)
}
func (s *Server) serveByWS(ln net.Listener, rpcPath string) {
s.ln = ln
if rpcPath == "" {
rpcPath = share.DefaultRPCPath
}
mux := http.NewServeMux()
mux.Handle(rpcPath, websocket.Handler(s.ServeWS))
srv := &http.Server{Handler: mux}
srv.Serve(ln)
}
func (s *Server) sendResponse(ctx *share.Context, conn net.Conn, err error, req, res *protocol.Message) {
if len(res.Payload) > 1024 && req.CompressType() != protocol.None {
res.SetCompressType(req.CompressType())
}
s.Plugins.DoPreWriteResponse(ctx, req, res, err)
data := res.EncodeSlicePointer()
if s.AsyncWrite {
if s.pool != nil {
s.pool.Submit(func() {
if s.writeTimeout != 0 {
conn.SetWriteDeadline(time.Now().Add(s.writeTimeout))
}
conn.Write(*data)
protocol.PutData(data)
})
} else {
go func() {
if s.writeTimeout != 0 {
conn.SetWriteDeadline(time.Now().Add(s.writeTimeout))
}
conn.Write(*data)
protocol.PutData(data)
}()
}
} else {
if s.writeTimeout != 0 {
conn.SetWriteDeadline(time.Now().Add(s.writeTimeout))
}
conn.Write(*data)
protocol.PutData(data)
}
s.Plugins.DoPostWriteResponse(ctx, req, res, err)
}
func (s *Server) serveConn(conn net.Conn) {
if s.isShutdown() {
s.closeConn(conn)
return
}
defer func() {
if err := recover(); err != nil {
const size = 64 << 10
buf := make([]byte, size)
ss := runtime.Stack(buf, false)
if ss > size {
ss = size
}
buf = buf[:ss]
log.Errorf("serving %s panic error: %s, stack:\n %s", conn.RemoteAddr(), err, buf)
}
if share.Trace {
log.Debugf("server closed conn: %v", conn.RemoteAddr().String())
}
// make sure all inflight requests are handled and all drained
if s.isShutdown() {
<-s.doneChan
}
s.closeConn(conn)
}()
if tlsConn, ok := conn.(*tls.Conn); ok {
if d := s.readTimeout; d != 0 {
conn.SetReadDeadline(time.Now().Add(d))
}
if d := s.writeTimeout; d != 0 {
conn.SetWriteDeadline(time.Now().Add(d))
}
if err := tlsConn.Handshake(); err != nil {
log.Errorf("rpcx: TLS handshake error from %s: %v", conn.RemoteAddr(), err)
return
}
}
r := bufio.NewReaderSize(conn, ReaderBuffsize)
// read requests and handle it
for {
if s.isShutdown() {
return
}
t0 := time.Now()
if s.readTimeout != 0 {
conn.SetReadDeadline(t0.Add(s.readTimeout))
}
// create a rpcx Context
ctx := share.WithValue(context.Background(), RemoteConnContextKey, conn)
// read a request from the underlying connection
req, err := s.readRequest(ctx, r)
if err != nil {
if errors.Is(err, io.EOF) {
log.Infof("client has closed this connection: %s", conn.RemoteAddr().String())
} else if errors.Is(err, net.ErrClosed) {
log.Infof("rpcx: connection %s is closed", conn.RemoteAddr().String())
} else if errors.Is(err, ErrReqReachLimit) {
if !req.IsOneway() { // return a error response
res := req.Clone()
res.SetMessageType(protocol.Response)
s.handleError(res, err)
s.sendResponse(ctx, conn, err, req, res)
} else { // Oneway and only call the plugins
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
}
continue
} else { // wrong data
log.Warnf("rpcx: failed to read request: %v", err)
}
if s.HandleServiceError != nil {
s.HandleServiceError(err)
}
return
}
if share.Trace {
log.Debugf("server received an request %+v from conn: %v", req, conn.RemoteAddr().String())
}
ctx = share.WithLocalValue(ctx, StartRequestContextKey, time.Now().UnixNano())
closeConn := false
if !req.IsHeartbeat() {
err = s.auth(ctx, req)
closeConn = err != nil
}
if err != nil {
if !req.IsOneway() { // return a error response
res := req.Clone()
res.SetMessageType(protocol.Response)
s.handleError(res, err)
s.sendResponse(ctx, conn, err, req, res)
} else {
s.Plugins.DoPreWriteResponse(ctx, req, nil, err)
}
if s.HandleServiceError != nil {
s.HandleServiceError(err)
}
// auth failed, closed the connection
if closeConn {
log.Infof("auth failed for conn %s: %v", conn.RemoteAddr().String(), err)
return
}
continue
}
if s.pool != nil {
s.pool.Submit(func() {
s.processOneRequest(ctx, req, conn)
})
} else {
go s.processOneRequest(ctx, req, conn)
}
}
}
func (s *Server) processOneRequest(ctx *share.Context, req *protocol.Message, conn net.Conn) {
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 1024)
buf = buf[:runtime.Stack(buf, true)]
if s.HandleServiceError != nil {
s.HandleServiceError(fmt.Errorf("%v", r))
} else {
log.Errorf("[handler internal error]: servicepath: %s, servicemethod: %s, err: %v,stacks: %s", req.ServicePath, req.ServiceMethod, r, string(buf))
}
sctx := NewContext(ctx, conn, req, s.AsyncWrite)
sctx.WriteError(fmt.Errorf("%v", r))
}
}()
atomic.AddInt32(&s.handlerMsgNum, 1)
defer atomic.AddInt32(&s.handlerMsgNum, -1)
// 心跳请求,直接处理返回
if req.IsHeartbeat() {
s.Plugins.DoHeartbeatRequest(ctx, req)
req.SetMessageType(protocol.Response)
data := req.EncodeSlicePointer()
if s.writeTimeout != 0 {
conn.SetWriteDeadline(time.Now().Add(s.writeTimeout))
}
conn.Write(*data)
protocol.PutData(data)
return
}
cancelFunc := parseServerTimeout(ctx, req)
if cancelFunc != nil {
defer cancelFunc()
}
resMetadata := make(map[string]string)
if req.Metadata == nil {
req.Metadata = make(map[string]string)
}
ctx = share.WithLocalValue(share.WithLocalValue(ctx, share.ReqMetaDataKey, req.Metadata),
share.ResMetaDataKey, resMetadata)
s.Plugins.DoPreHandleRequest(ctx, req)
if share.Trace {
log.Debugf("server handle request %+v from conn: %v", req, conn.RemoteAddr().String())
}
// use handlers first
if handler, ok := s.router[req.ServicePath+"."+req.ServiceMethod]; ok {
sctx := NewContext(ctx, conn, req, s.AsyncWrite)
err := handler(sctx)
if err != nil {
if s.HandleServiceError != nil {
s.HandleServiceError(err)
} else {
log.Errorf("[handler internal error]: servicepath: %s, servicemethod, err: %v", req.ServicePath, req.ServiceMethod, err)
}
sctx.WriteError(err)
}
return
}
res, err := s.handleRequest(ctx, req)
if err != nil {
if s.HandleServiceError != nil {
s.HandleServiceError(err)
} else {
log.Warnf("rpcx: failed to handle request: %v", err)
}
}
if !req.IsOneway() {
if len(resMetadata) > 0 { // copy meta in context to responses
meta := res.Metadata
if meta == nil {
res.Metadata = resMetadata
} else {
for k, v := range resMetadata {
if meta[k] == "" {
meta[k] = v
}
}
}
}
s.sendResponse(ctx, conn, err, req, res)
}
if share.Trace {
log.Debugf("server write response %+v for an request %+v from conn: %v", res, req, conn.RemoteAddr().String())
}
}
func parseServerTimeout(ctx *share.Context, req *protocol.Message) context.CancelFunc {
if req == nil || req.Metadata == nil {
return nil
}
st := req.Metadata[share.ServerTimeout]
if st == "" {
return nil
}
timeout, err := strconv.ParseInt(st, 10, 64)
if err != nil {
return nil
}
newCtx, cancel := context.WithTimeout(ctx.Context, time.Duration(timeout)*time.Millisecond)
ctx.Context = newCtx
return cancel
}
func (s *Server) isShutdown() bool {
return atomic.LoadInt32(&s.inShutdown) == 1
}
func (s *Server) closeConn(conn net.Conn) {
s.mu.Lock()
delete(s.activeConn, conn)
s.mu.Unlock()
conn.Close()
s.Plugins.DoPostConnClose(conn)
}
func (s *Server) readRequest(ctx context.Context, r io.Reader) (req *protocol.Message, err error) {
err = s.Plugins.DoPreReadRequest(ctx)
if err != nil {
return nil, err
}
// pool req?
req = protocol.NewMessage()
err = req.Decode(r)
if err == io.EOF {
return req, err
}
perr := s.Plugins.DoPostReadRequest(ctx, req, err)
if err == nil {
err = perr
}
return req, err
}
func (s *Server) auth(ctx context.Context, req *protocol.Message) error {
if s.AuthFunc != nil {
token := req.Metadata[share.AuthKey]
return s.AuthFunc(ctx, req, token)
}
return nil
}
func (s *Server) handleRequest(ctx context.Context, req *protocol.Message) (res *protocol.Message, err error) {
serviceName := req.ServicePath
methodName := req.ServiceMethod
res = req.Clone()
res.SetMessageType(protocol.Response)
s.serviceMapMu.RLock()
service := s.serviceMap[serviceName]
if share.Trace {
log.Debugf("server get service %+v for an request %+v", service, req)
}
s.serviceMapMu.RUnlock()
if service == nil {
err = errors.New("rpcx: can't find service " + serviceName)
return s.handleError(res, err)
}
mtype := service.method[methodName]
if mtype == nil {
if service.function[methodName] != nil { // check raw functions
return s.handleRequestForFunction(ctx, req)
}
err = errors.New("rpcx: can't find method " + methodName)
return s.handleError(res, err)
}
// get a argv object from object pool
argv := reflectTypePools.Get(mtype.ArgType)
codec := share.Codecs[req.SerializeType()]
if codec == nil {
err = fmt.Errorf("can not find codec for %d", req.SerializeType())
return s.handleError(res, err)
}
err = codec.Decode(req.Payload, argv)
if err != nil {
return s.handleError(res, err)
}
// and get a reply object from object pool
replyv := reflectTypePools.Get(mtype.ReplyType)
argv, err = s.Plugins.DoPreCall(ctx, serviceName, methodName, argv)
if err != nil {
// return reply to object pool
reflectTypePools.Put(mtype.ReplyType, replyv)
return s.handleError(res, err)
}
if mtype.ArgType.Kind() != reflect.Ptr {
err = service.call(ctx, mtype, reflect.ValueOf(argv).Elem(), reflect.ValueOf(replyv))
} else {
err = service.call(ctx, mtype, reflect.ValueOf(argv), reflect.ValueOf(replyv))
}
replyv, err1 := s.Plugins.DoPostCall(ctx, serviceName, methodName, argv, replyv, err)
if err == nil {
err = err1
}
// return argc to object pool
reflectTypePools.Put(mtype.ArgType, argv)
if err != nil {
if replyv != nil {
data, err := codec.Encode(replyv)
// return reply to object pool
reflectTypePools.Put(mtype.ReplyType, replyv)
if err != nil {
return s.handleError(res, err)
}
res.Payload = data
}
return s.handleError(res, err)
}
if !req.IsOneway() {
data, err := codec.Encode(replyv)
// return reply to object pool
reflectTypePools.Put(mtype.ReplyType, replyv)
if err != nil {
return s.handleError(res, err)
}
res.Payload = data
} else if replyv != nil {
reflectTypePools.Put(mtype.ReplyType, replyv)
}
if share.Trace {
log.Debugf("server called service %+v for an request %+v", service, req)
}
return res, nil
}
func (s *Server) handleRequestForFunction(ctx context.Context, req *protocol.Message) (res *protocol.Message, err error) {
res = req.Clone()
res.SetMessageType(protocol.Response)
serviceName := req.ServicePath
methodName := req.ServiceMethod
s.serviceMapMu.RLock()
service := s.serviceMap[serviceName]
s.serviceMapMu.RUnlock()
if service == nil {
err = errors.New("rpcx: can't find service for func raw function")
return s.handleError(res, err)
}
mtype := service.function[methodName]
if mtype == nil {
err = errors.New("rpcx: can't find method " + methodName)
return s.handleError(res, err)
}
argv := reflectTypePools.Get(mtype.ArgType)
codec := share.Codecs[req.SerializeType()]
if codec == nil {
err = fmt.Errorf("can not find codec for %d", req.SerializeType())
return s.handleError(res, err)
}
err = codec.Decode(req.Payload, argv)
if err != nil {
return s.handleError(res, err)
}
replyv := reflectTypePools.Get(mtype.ReplyType)
argv, err = s.Plugins.DoPreCall(ctx, serviceName, methodName, argv)
if err != nil {
// return reply to object pool
reflectTypePools.Put(mtype.ReplyType, replyv)
return s.handleError(res, err)
}
if mtype.ArgType.Kind() != reflect.Ptr {
err = service.callForFunction(ctx, mtype, reflect.ValueOf(argv).Elem(), reflect.ValueOf(replyv))
} else {
err = service.callForFunction(ctx, mtype, reflect.ValueOf(argv), reflect.ValueOf(replyv))
}
replyv, err1 := s.Plugins.DoPostCall(ctx, serviceName, methodName, argv, replyv, err)
if err == nil {
err = err1
}
reflectTypePools.Put(mtype.ArgType, argv)
if err != nil {
reflectTypePools.Put(mtype.ReplyType, replyv)
return s.handleError(res, err)
}
if !req.IsOneway() {
data, err := codec.Encode(replyv)
reflectTypePools.Put(mtype.ReplyType, replyv)
if err != nil {
return s.handleError(res, err)
}
res.Payload = data
} else if replyv != nil {
reflectTypePools.Put(mtype.ReplyType, replyv)
}
return res, nil
}
func (s *Server) handleError(res *protocol.Message, err error) (*protocol.Message, error) {
res.SetMessageStatusType(protocol.Error)
if res.Metadata == nil {
res.Metadata = make(map[string]string)
}
if s.ServerErrorFunc != nil {
res.Metadata[protocol.ServiceError] = s.ServerErrorFunc(res, err)
} else {
res.Metadata[protocol.ServiceError] = err.Error()
}
return res, err
}
// Can connect to RPC service using HTTP CONNECT to rpcPath.
var connected = "200 Connected to rpcx"
// ServeHTTP implements an http.Handler that answers RPC requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodConnect {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusMethodNotAllowed)
io.WriteString(w, "405 must CONNECT\n")
return
}
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
log.Info("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
return
}
io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
s.mu.Lock()
s.activeConn[conn] = struct{}{}
s.mu.Unlock()
s.serveConn(conn)
}
func (s *Server) ServeWS(conn *websocket.Conn) {
s.mu.Lock()
s.activeConn[conn] = struct{}{}
s.mu.Unlock()
conn.PayloadType = websocket.BinaryFrame
s.serveConn(conn)
}
// Close immediately closes all active net.Listeners.
func (s *Server) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
var err error
if s.ln != nil {
err = s.ln.Close()
}
for c := range s.activeConn {
c.Close()
delete(s.activeConn, c)
s.Plugins.DoPostConnClose(c)
}
s.closeDoneChanLocked()
if s.pool != nil {
s.pool.StopAndWaitFor(10 * time.Second)
}
return err
}
// RegisterOnShutdown registers a function to call on Shutdown.
// This can be used to gracefully shutdown connections.
func (s *Server) RegisterOnShutdown(f func(s *Server)) {
s.mu.Lock()
s.onShutdown = append(s.onShutdown, f)
s.mu.Unlock()
}
// RegisterOnRestart registers a function to call on Restart.
func (s *Server) RegisterOnRestart(f func(s *Server)) {
s.mu.Lock()
s.onRestart = append(s.onRestart, f)
s.mu.Unlock()
}
var shutdownPollInterval = 1000 * time.Millisecond
// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing the
// listener, then closing all idle connections, and then waiting
// indefinitely for connections to return to idle and then shut down.
// If the provided context expires before the shutdown is complete,
// Shutdown returns the context's error, otherwise it returns any
// error returned from closing the Server's underlying Listener.
func (s *Server) Shutdown(ctx context.Context) error {
var err error
if atomic.CompareAndSwapInt32(&s.inShutdown, 0, 1) {
log.Info("shutdown begin")
s.mu.Lock()
// 主动注销注册的服务
s.UnregisterAll()
if s.ln != nil {
s.ln.Close()
}
for conn := range s.activeConn {
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.CloseRead()
}
}
s.mu.Unlock()
// wait all in-processing requests finish.
ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
outer:
for {
if s.checkProcessMsg() {
break
}
select {
case <-ctx.Done():
err = ctx.Err()
break outer
case <-ticker.C:
}
}
s.jsonrpcHTTPServerLock.Lock()
if s.gatewayHTTPServer != nil {
if err := s.closeHTTP1APIGateway(ctx); err != nil {
log.Warnf("failed to close gateway: %v", err)
} else {
log.Info("closed gateway")
}
}
s.jsonrpcHTTPServerLock.Unlock()
if s.jsonrpcHTTPServer != nil {
if err := s.closeJSONRPC2(ctx); err != nil {
log.Warnf("failed to close JSONRPC: %v", err)
} else {
log.Info("closed JSONRPC")
}
}
s.mu.Lock()
for conn := range s.activeConn {
conn.Close()
delete(s.activeConn, conn)
s.Plugins.DoPostConnClose(conn)
}
s.closeDoneChanLocked()
s.mu.Unlock()
log.Info("shutdown end")
}
return err
}
// Restart restarts this server gracefully.
// It starts a new rpcx server with the same port with SO_REUSEPORT socket option,
// and shutdown this rpcx server gracefully.
func (s *Server) Restart(ctx context.Context) error {
pid, err := s.startProcess()
if err != nil {
return err
}
log.Infof("restart a new rpcx server: %d", pid)
// TODO: is it necessary?
time.Sleep(3 * time.Second)
return s.Shutdown(ctx)
}
func (s *Server) startProcess() (int, error) {
argv0, err := exec.LookPath(os.Args[0])
if err != nil {
return 0, err
}
// Pass on the environment and replace the old count key with the new one.
var env []string
env = append(env, os.Environ()...)
originalWD, _ := os.Getwd()
allFiles := []*os.File{os.Stdin, os.Stdout, os.Stderr}
process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{
Dir: originalWD,
Env: env,
Files: allFiles,
})
if err != nil {
return 0, err
}
return process.Pid, nil
}
func (s *Server) checkProcessMsg() bool {
size := atomic.LoadInt32(&s.handlerMsgNum)
log.Info("need handle in-processing msg size:", size)
return size == 0
}
func (s *Server) closeDoneChanLocked() {
select {
case <-s.doneChan:
// Already closed. Don't close again.
default:
// Safe to close here. We're the only closer, guarded
// by s.mu.RegisterName
close(s.doneChan)
}
}
var ip4Reg = regexp.MustCompile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)
func validIP4(ipAddress string) bool {
ipAddress = strings.Trim(ipAddress, " ")
i := strings.LastIndex(ipAddress, ":")
ipAddress = ipAddress[:i] // remove port
return ip4Reg.MatchString(ipAddress)
}
func validIP6(ipAddress string) bool {
ipAddress = strings.Trim(ipAddress, " ")
i := strings.LastIndex(ipAddress, ":")
ipAddress = ipAddress[:i] // remove port
ipAddress = strings.TrimPrefix(ipAddress, "[")
ipAddress = strings.TrimSuffix(ipAddress, "]")
ip := net.ParseIP(ipAddress)
if ip != nil && ip.To4() == nil {
return true
} else {
return false
}
}
================================================
FILE: server/server_test.go
================================================
package server
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"testing"
"time"
testutils "github.com/smallnest/rpcx/_testutils"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/share"
"github.com/stretchr/testify/assert"
)
type Args struct {
A int
B int
}
type Reply struct {
C int
}
type Arith int
func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error {
reply.C = args.A * args.B
return nil
}
func (t *Arith) ThriftMul(ctx context.Context, args *testutils.ThriftArgs_, reply *testutils.ThriftReply) error {
reply.C = args.A * args.B
return nil
}
func (t *Arith) ConsumingOperation(ctx context.Context, args *testutils.ThriftArgs_, reply *testutils.ThriftReply) error {
reply.C = args.A * args.B
time.Sleep(10 * time.Second)
return nil
}
func TestShutdownHook(t *testing.T) {
s := NewServer()
var cancel1 context.CancelFunc
s.RegisterOnShutdown(func(s *Server) {
var ctx context.Context
ctx, cancel1 = context.WithTimeout(context.Background(), 155*time.Second)
s.Shutdown(ctx)
})
s.RegisterName("Arith2", new(Arith), "")
s.Register(new(Arith), "")
go s.Serve("tcp", ":0")
time.Sleep(time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
s.Shutdown(ctx)
cancel()
if cancel1 != nil {
cancel1()
}
}
func TestHandleRequest(t *testing.T) {
// use jsoncodec
req := protocol.NewMessage()
req.SetVersion(0)
req.SetMessageType(protocol.Request)
req.SetHeartbeat(false)
req.SetOneway(false)
req.SetCompressType(protocol.None)
req.SetMessageStatusType(protocol.Normal)
req.SetSerializeType(protocol.JSON)
req.SetSeq(1234567890)
req.ServicePath = "Arith"
req.ServiceMethod = "Mul"
argv := &Args{
A: 10,
B: 20,
}
data, err := json.Marshal(argv)
if err != nil {
t.Fatal(err)
}
req.Payload = data
server := NewServer()
server.RegisterName("Arith", new(Arith), "")
res, err := server.handleRequest(context.Background(), req)
if err != nil {
t.Fatalf("failed to hand request: %v", err)
}
if res.Payload == nil {
t.Fatalf("expect reply but got %s", res.Payload)
}
reply := &Reply{}
codec := share.Codecs[res.SerializeType()]
if codec == nil {
t.Fatalf("can not find codec %c", codec)
}
err = codec.Decode(res.Payload, reply)
if err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if reply.C != 200 {
t.Fatalf("expect 200 but got %d", reply.C)
}
}
func TestHandler(t *testing.T) {
// use jsoncodec
req := protocol.NewMessage()
req.SetVersion(0)
req.SetMessageType(protocol.Request)
req.SetHeartbeat(false)
req.SetOneway(false)
req.SetCompressType(protocol.None)
req.SetMessageStatusType(protocol.Normal)
req.SetSerializeType(protocol.JSON)
req.SetSeq(1234567890)
req.ServicePath = "Arith"
req.ServiceMethod = "Mul"
argv := &Args{
A: 10,
B: 20,
}
data, err := json.Marshal(argv)
if err != nil {
t.Fatal(err)
}
req.Payload = data
serverConn, clientConn := net.Pipe()
handler := func(ctx *Context) error {
req := &Args{}
res := &Reply{}
ctx.Bind(req)
res.C = req.A * req.B
return ctx.Write(res)
}
go func() {
ctx := NewContext(share.NewContext(context.Background()), serverConn, req, false)
err = handler(ctx)
assert.NoError(t, err)
serverConn.Close()
}()
data, err = io.ReadAll(clientConn)
assert.NoError(t, err)
resp, err := protocol.Read(bytes.NewReader(data))
assert.NoError(t, err)
assert.Equal(t, "Arith", resp.ServicePath)
assert.Equal(t, "Mul", resp.ServiceMethod)
assert.Equal(t, req.Seq(), resp.Seq())
assert.Equal(t, "{\"C\":200}", string(resp.Payload))
}
func Test_validIP6(t *testing.T) {
type args struct {
ipAddress string
}
tests := []struct {
name string
args args
want bool
}{
{"1", args{ipAddress: "[CDCD:910A:2222:5498:8475:1111:3900:2020]:8080"}, true},
{"2", args{ipAddress: "[1030::C9B4:FF12:48AA:1A2B]:8080"}, true},
{"3", args{ipAddress: "[2000:0:0:0:0:0:0:1]:8080"}, true},
{"4", args{ipAddress: "127.0.0.1:8080"}, false},
{"5", args{ipAddress: "localhost:8080"}, false},
{"6", args{ipAddress: "127.1:8080"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, validIP6(tt.args.ipAddress), "validIP6(%v)", tt.args.ipAddress)
})
}
}
================================================
FILE: server/service.go
================================================
package server
import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
"strings"
"sync"
"unicode"
"unicode/utf8"
rerrors "github.com/smallnest/rpcx/errors"
"github.com/smallnest/rpcx/log"
)
// RpcServiceError represents an error that is case by service implementation.
type RpcServiceInternalError struct {
Err string
Method string
Argv interface{}
stack string
}
// Error returns the error message.
func (e RpcServiceInternalError) Error() string {
return fmt.Sprintf("[service internal error]: %v, method: %s, argv: %+v, stack: %s", e.Err, e.Method, e.Argv, e.stack)
}
// String returns the error message.
func (e RpcServiceInternalError) String() string {
return e.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.TypeFor[error]()
// Precompute the reflect type for context.
var typeOfContext = reflect.TypeFor[context.Context]()
type methodType struct {
sync.Mutex // protects counters
method reflect.Method
ArgType reflect.Type
ReplyType reflect.Type
// numCalls uint
}
type functionType struct {
sync.Mutex // protects counters
fn reflect.Value
ArgType reflect.Type
ReplyType reflect.Type
}
type service struct {
name string // name of service
rcvr reflect.Value // receiver of methods for the service
typ reflect.Type // type of the receiver
method map[string]*methodType // registered methods
function map[string]*functionType // registered functions
}
func isExported(name string) bool {
rune, _ := utf8.DecodeRuneInString(name)
return unicode.IsUpper(rune)
}
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() == ""
}
func (s *Server) ListServices() []string {
s.serviceMapMu.RLock()
defer s.serviceMapMu.RUnlock()
var arr []string
for name := range s.serviceMap {
arr = append(arr, name)
}
return arr
}
// Register publishes in the server the set of methods of the
// receiver value that satisfy the following conditions:
// - exported method of exported type
// - three arguments, the first is of context.Context, both of exported type for three arguments
// - the third argument is a pointer
// - one return value, of type error
//
// It returns an error if the receiver is not an exported type or has
// no suitable methods. It also logs the error.
// The client accesses each method using a string of the form "Type.Method",
// where Type is the receiver's concrete type.
func (s *Server) Register(rcvr interface{}, metadata string) error {
sname, err := s.register(rcvr, "", false)
if err != nil {
return err
}
return s.Plugins.DoRegister(sname, rcvr, metadata)
}
// RegisterName is like Register but uses the provided name for the type
// instead of the receiver's concrete type.
func (s *Server) RegisterName(name string, rcvr interface{}, metadata string) error {
_, err := s.register(rcvr, name, true)
if err != nil {
return err
}
if s.Plugins == nil {
s.Plugins = &pluginContainer{}
}
return s.Plugins.DoRegister(name, rcvr, metadata)
}
// RegisterFunction publishes a function that satisfy the following conditions:
// - three arguments, the first is of context.Context, both of exported type for three arguments
// - the third argument is a pointer
// - one return value, of type error
//
// The client accesses function using a string of the form "servicePath.Method".
func (s *Server) RegisterFunction(servicePath string, fn interface{}, metadata string) error {
fname, err := s.registerFunction(servicePath, fn, "", false)
if err != nil {
return err
}
return s.Plugins.DoRegisterFunction(servicePath, fname, fn, metadata)
}
// RegisterFunctionName is like RegisterFunction but uses the provided name for the function
// instead of the function's concrete type.
func (s *Server) RegisterFunctionName(servicePath string, name string, fn interface{}, metadata string) error {
_, err := s.registerFunction(servicePath, fn, name, true)
if err != nil {
return err
}
return s.Plugins.DoRegisterFunction(servicePath, name, fn, metadata)
}
func (s *Server) register(rcvr interface{}, name string, useName bool) (string, error) {
s.serviceMapMu.Lock()
defer s.serviceMapMu.Unlock()
service := new(service)
service.typ = reflect.TypeOf(rcvr)
service.rcvr = reflect.ValueOf(rcvr)
sname := reflect.Indirect(service.rcvr).Type().Name() // Type
if useName {
sname = name
}
if sname == "" {
errorStr := "rpcx.Register: no service name for type " + service.typ.String()
log.Error(errorStr)
return sname, errors.New(errorStr)
}
if !useName && !isExported(sname) {
errorStr := "rpcx.Register: type " + sname + " is not exported"
log.Error(errorStr)
return sname, errors.New(errorStr)
}
service.name = sname
// Install the methods
service.method = suitableMethods(service.typ, true)
if len(service.method) == 0 {
var errorStr string
// To help the user, see if a pointer receiver would work.
method := suitableMethods(reflect.PtrTo(service.typ), false)
if len(method) != 0 {
errorStr = "rpcx.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)"
} else {
errorStr = "rpcx.Register: type " + sname + " has no exported methods of suitable type"
}
log.Error(errorStr)
return sname, errors.New(errorStr)
}
s.serviceMap[service.name] = service
return sname, nil
}
func (s *Server) registerFunction(servicePath string, fn interface{}, name string, useName bool) (string, error) {
s.serviceMapMu.Lock()
defer s.serviceMapMu.Unlock()
ss := s.serviceMap[servicePath]
if ss == nil {
ss = new(service)
ss.name = servicePath
ss.function = make(map[string]*functionType)
}
f, ok := fn.(reflect.Value)
if !ok {
f = reflect.ValueOf(fn)
}
if f.Kind() != reflect.Func {
return "", errors.New("function must be func or bound method")
}
fname := runtime.FuncForPC(reflect.Indirect(f).Pointer()).Name()
if fname != "" {
i := strings.LastIndex(fname, ".")
if i >= 0 {
fname = fname[i+1:]
}
}
if useName {
fname = name
}
if fname == "" {
errorStr := "rpcx.registerFunction: no func name for type " + f.Type().String()
log.Error(errorStr)
return fname, errors.New(errorStr)
}
t := f.Type()
if t.NumIn() != 3 {
return fname, fmt.Errorf("rpcx.registerFunction: has wrong number of ins: %s", f.Type().String())
}
if t.NumOut() != 1 {
return fname, fmt.Errorf("rpcx.registerFunction: has wrong number of outs: %s", f.Type().String())
}
// First arg must be context.Context
ctxType := t.In(0)
if !ctxType.Implements(typeOfContext) {
return fname, fmt.Errorf("function %s must use context as the first parameter", f.Type().String())
}
argType := t.In(1)
if !isExportedOrBuiltinType(argType) {
return fname, fmt.Errorf("function %s parameter type not exported: %v", f.Type().String(), argType)
}
replyType := t.In(2)
if replyType.Kind() != reflect.Ptr {
return fname, fmt.Errorf("function %s reply type not a pointer: %s", f.Type().String(), replyType)
}
if !isExportedOrBuiltinType(replyType) {
return fname, fmt.Errorf("function %s reply type not exported: %v", f.Type().String(), replyType)
}
// The return type of the method must be error.
if returnType := t.Out(0); returnType != typeOfError {
return fname, fmt.Errorf("function %s returns %s, not error", f.Type().String(), returnType.String())
}
// Install the methods
ss.function[fname] = &functionType{fn: f, ArgType: argType, ReplyType: replyType}
s.serviceMap[servicePath] = ss
// init pool for reflect.Type of args and reply
reflectTypePools.Init(argType)
reflectTypePools.Init(replyType)
return fname, nil
}
// suitableMethods returns suitable Rpc methods of typ, it will report
// error using log if reportErr is true.
func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType {
methods := make(map[string]*methodType)
for m := 0; m < typ.NumMethod(); m++ {
method := typ.Method(m)
mtype := method.Type
mname := method.Name
// Method must be exported.
if method.PkgPath != "" {
continue
}
// Method needs four ins: receiver, context.Context, *args, *reply.
if mtype.NumIn() != 4 {
if reportErr {
log.Debug("method ", mname, " has wrong number of ins:", mtype.NumIn())
}
continue
}
// First arg must be context.Context
ctxType := mtype.In(1)
if !ctxType.Implements(typeOfContext) {
if reportErr {
log.Debug("method ", mname, " must use context.Context as the first parameter")
}
continue
}
// Second arg need not be a pointer.
argType := mtype.In(2)
if !isExportedOrBuiltinType(argType) {
if reportErr {
log.Info(mname, " parameter type not exported: ", argType)
}
continue
}
// Third arg must be a pointer.
replyType := mtype.In(3)
if replyType.Kind() != reflect.Ptr {
if reportErr {
log.Info("method", mname, " reply type not a pointer:", replyType)
}
continue
}
// Reply type must be exported.
if !isExportedOrBuiltinType(replyType) {
if reportErr {
log.Info("method", mname, " reply type not exported:", replyType)
}
continue
}
// Method needs one out.
if mtype.NumOut() != 1 {
if reportErr {
log.Info("method", mname, " has wrong number of outs:", mtype.NumOut())
}
continue
}
// The return type of the method must be error.
if returnType := mtype.Out(0); returnType != typeOfError {
if reportErr {
log.Info("method", mname, " returns ", returnType.String(), " not error")
}
continue
}
methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
// init pool for reflect.Type of args and reply
reflectTypePools.Init(argType)
reflectTypePools.Init(replyType)
}
return methods
}
// UnregisterAll unregisters all services.
// You can call this method when you want to shutdown/upgrade this node.
func (s *Server) UnregisterAll() error {
var err error
s.unregisterAllOnce.Do(func() {
err = s.unregisterAll()
})
return err
}
func (s *Server) unregisterAll() error {
s.serviceMapMu.RLock()
defer s.serviceMapMu.RUnlock()
var es []error
for k := range s.serviceMap {
err := s.Plugins.DoUnregister(k)
if err != nil {
es = append(es, err)
}
}
if len(es) > 0 {
return rerrors.NewMultiError(es)
}
return nil
}
func (s *service) call(ctx context.Context, mtype *methodType, argv, replyv reflect.Value) (err error) {
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
buf = buf[:n]
err = &RpcServiceInternalError{
Err: fmt.Sprintf("%v", r),
Method: mtype.method.Name,
Argv: argv.Interface(),
stack: string(buf),
}
log.Error(err)
}
}()
function := mtype.method.Func
// Invoke the method, providing a new value for the reply.
returnValues := function.Call([]reflect.Value{s.rcvr, reflect.ValueOf(ctx), argv, replyv})
// The return value for the method is an error.
errInter := returnValues[0].Interface()
if errInter != nil {
return errInter.(error)
}
return nil
}
func (s *service) callForFunction(ctx context.Context, ft *functionType, argv, replyv reflect.Value) (err error) {
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
buf = buf[:n]
err = &RpcServiceInternalError{
Err: fmt.Sprintf("%v", r),
Method: fmt.Sprintf("%s", runtime.FuncForPC(ft.fn.Pointer())),
Argv: argv.Interface(),
stack: string(buf),
}
log.Error(err)
}
}()
// Invoke the function, providing a new value for the reply.
returnValues := ft.fn.Call([]reflect.Value{reflect.ValueOf(ctx), argv, replyv})
// The return value for the method is an error.
errInter := returnValues[0].Interface()
if errInter != nil {
return errInter.(error)
}
return nil
}
================================================
FILE: server/service_test.go
================================================
package server
import (
"context"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_isExported(t *testing.T) {
assert.Equal(t, true, isExported("IsExported"))
assert.Equal(t, false, isExported("isExported"))
assert.Equal(t, false, isExported("_isExported"))
assert.Equal(t, false, isExported("123_isExported"))
assert.Equal(t, false, isExported("[]_isExported"))
assert.Equal(t, false, isExported("&_isExported"))
}
func Mul(ctx context.Context, args *Args, reply *Reply) error {
reply.C = args.A * args.B
return nil
}
func Test_isExportedOrBuiltinType(t *testing.T) {
typeOfMul := reflect.TypeOf(Mul)
assert.Equal(t, true, isExportedOrBuiltinType(typeOfMul))
}
================================================
FILE: server/stream.go
================================================
package server
import (
"context"
"crypto/rand"
"errors"
"io"
"net"
"sync"
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/smallnest/rpcx/log"
"github.com/smallnest/rpcx/share"
)
var ErrNotAccept = errors.New("server refused the connection")
// StreamHandler handles a streaming connection with client.
type StreamHandler func(conn net.Conn, args *share.StreamServiceArgs)
// StreamAcceptor accepts connection from clients or not.
// You can use it to validate clients and determine if accept or drop the connection.
type StreamAcceptor func(ctx context.Context, args *share.StreamServiceArgs) bool
type streamTokenInfo struct {
token []byte
args *share.StreamServiceArgs
}
// StreamService support streaming between clients and server.
// It registers a streaming service and listens on the given port.
// Clients will invokes this service to get the token and send the token and begin to stream.
type StreamService struct {
Addr string
AdvertiseAddr string
handler StreamHandler
acceptor StreamAcceptor
cachedTokens *lru.Cache
startOnce sync.Once
ln net.Listener
done chan struct{}
}
// NewStreamService creates a stream service.
func NewStreamService(addr string, streamHandler StreamHandler, acceptor StreamAcceptor, waitNum int) *StreamService {
cachedTokens, _ := lru.New(waitNum)
fi := &StreamService{
Addr: addr,
handler: streamHandler,
cachedTokens: cachedTokens,
acceptor: acceptor,
}
return fi
}
// EnableStreamService supports stream service in this server.
func (s *Server) EnableStreamService(serviceName string, streamService *StreamService) {
if serviceName == "" {
serviceName = share.StreamServiceName
}
_ = streamService.Start()
_ = s.RegisterName(serviceName, streamService, "")
}
func (s *StreamService) Stream(ctx context.Context, args *share.StreamServiceArgs, reply *share.StreamServiceReply) error {
// clientConn := ctx.Value(server.RemoteConnContextKey).(net.Conn)
if s.acceptor != nil && !s.acceptor(ctx, args) {
return ErrNotAccept
}
token := make([]byte, 32)
_, err := rand.Read(token)
if err != nil {
return err
}
*reply = share.StreamServiceReply{
Token: token,
Addr: s.Addr,
}
if s.AdvertiseAddr != "" {
reply.Addr = s.AdvertiseAddr
}
s.cachedTokens.Add(string(token), &streamTokenInfo{token, args})
return nil
}
func (s *StreamService) Start() error {
s.startOnce.Do(func() {
go s.start()
})
return nil
}
func (s *StreamService) start() error {
ln, err := net.Listen("tcp", s.Addr)
if err != nil {
return err
}
s.ln = ln
var tempDelay time.Duration
for {
select {
case <-s.done:
return nil
default:
conn, e := ln.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
log.Errorf("filetransfer: accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
if tc, ok := conn.(*net.TCPConn); ok {
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
tc.SetLinger(10)
}
token := make([]byte, 32)
_, err := io.ReadFull(conn, token)
if err != nil {
conn.Close()
log.Errorf("failed to read token from %s", conn.RemoteAddr().String())
continue
}
tokenStr := string(token)
info, ok := s.cachedTokens.Get(tokenStr)
if !ok {
conn.Close()
log.Errorf("failed to read token from %s", conn.RemoteAddr().String())
continue
}
s.cachedTokens.Remove(tokenStr)
switch ti := info.(type) {
case *streamTokenInfo:
if s.handler == nil {
conn.Close()
continue
}
go s.handler(conn, ti.args)
default:
conn.Close()
}
}
}
}
func (s *StreamService) Stop() error {
close(s.done)
if s.ln != nil {
s.ln.Close()
}
return nil
}
================================================
FILE: serverplugin/alias.go
================================================
package serverplugin
import (
"context"
"github.com/smallnest/rpcx/protocol"
)
var aliasAppliedKey = "__aliasAppliedKey"
type aliasPair struct {
servicePath, serviceMethod string
}
// AliasPlugin can be used to set aliases for services
type AliasPlugin struct {
Aliases map[string]*aliasPair
ReseverseAliases map[string]*aliasPair
}
// Alias sets a alias for the serviceMethod.
// For example Alias("anewpath&method", "Arith.mul")
func (p *AliasPlugin) Alias(aliasServicePath, aliasServiceMethod string, servicePath, serviceMethod string) {
p.Aliases[aliasServicePath+"."+aliasServiceMethod] = &aliasPair{
servicePath: servicePath,
serviceMethod: serviceMethod,
}
p.ReseverseAliases[servicePath+"."+serviceMethod] = &aliasPair{
servicePath: aliasServicePath,
serviceMethod: aliasServiceMethod,
}
}
// NewAliasPlugin creates a new NewAliasPlugin
func NewAliasPlugin() *AliasPlugin {
return &AliasPlugin{
Aliases: make(map[string]*aliasPair),
ReseverseAliases: make(map[string]*aliasPair),
}
}
// PostReadRequest converts the alias of this service.
func (p *AliasPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error {
var sp = r.ServicePath
var sm = r.ServiceMethod
k := sp + "." + sm
if p.Aliases != nil {
if pm := p.Aliases[k]; pm != nil {
r.ServicePath = pm.servicePath
r.ServiceMethod = pm.serviceMethod
if r.Metadata == nil {
r.Metadata = make(map[string]string)
}
r.Metadata[aliasAppliedKey] = "true"
}
}
return nil
}
// PreWriteResponse restore servicePath and serviceMethod.
func (p *AliasPlugin) PreWriteResponse(ctx context.Context, r *protocol.Message, res *protocol.Message) error {
if r.Metadata[aliasAppliedKey] != "true" {
return nil
}
var sp = r.ServicePath
var sm = r.ServiceMethod
k := sp + "." + sm
if p.ReseverseAliases != nil {
if pm := p.ReseverseAliases[k]; pm != nil {
r.ServicePath = pm.servicePath
r.ServiceMethod = pm.serviceMethod
delete(r.Metadata, aliasAppliedKey)
if res != nil {
res.ServicePath = pm.servicePath
res.ServiceMethod = pm.serviceMethod
}
}
}
return nil
}
================================================
FILE: serverplugin/blacklist.go
================================================
package serverplugin
import "net"
// BlacklistPlugin is a plugin that control only ip addresses in blacklist can **NOT** access services.
type BlacklistPlugin struct {
Blacklist map[string]bool
BlacklistMask []*net.IPNet // net.ParseCIDR("172.17.0.0/16") to get *net.IPNet
}
// HandleConnAccept check ip.
func (plugin *BlacklistPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
ip, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
return conn, true
}
if plugin.Blacklist[ip] {
return conn, false
}
remoteIP := net.ParseIP(ip)
for _, mask := range plugin.BlacklistMask {
if mask.Contains(remoteIP) {
return conn, false
}
}
return conn, true
}
================================================
FILE: serverplugin/mdns.go
================================================
package serverplugin
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/grandcat/zeroconf"
metrics "github.com/rcrowley/go-metrics"
)
type serviceMeta struct {
Service string `json:"service,omitempty"`
Meta string `json:"meta,omitempty"`
ServiceAddress string `json:"service_address,omitempty"`
}
// MDNSRegisterPlugin implements mdns/dns-sd registry.
type MDNSRegisterPlugin struct {
// service address, for example, tcp@127.0.0.1:8972, quic@127.0.0.1:1234
ServiceAddress string
port int
Metrics metrics.Registry
// Registered services
Services []*serviceMeta
UpdateInterval time.Duration
server *zeroconf.Server
domain string
dying chan struct{}
done chan struct{}
}
// NewMDNSRegisterPlugin return a new MDNSRegisterPlugin.
// If domain is empty, use "local." in default.
func NewMDNSRegisterPlugin(serviceAddress string, port int, m metrics.Registry, updateInterval time.Duration, domain string) *MDNSRegisterPlugin {
if domain == "" {
domain = "local."
}
return &MDNSRegisterPlugin{
ServiceAddress: serviceAddress,
port: port,
Metrics: m,
UpdateInterval: updateInterval,
domain: domain,
dying: make(chan struct{}),
done: make(chan struct{}),
}
}
// Start starts the mdns loop.
func (p *MDNSRegisterPlugin) Start() error {
if p.server == nil && len(p.Services) != 0 {
p.initMDNS()
}
if p.UpdateInterval > 0 {
go func() {
ticker := time.NewTicker(p.UpdateInterval)
defer ticker.Stop()
defer p.server.Shutdown()
for {
// refresh service TTL
select {
case <-p.dying:
close(p.done)
return
case <-ticker.C:
if p.server == nil && len(p.Services) == 0 {
break
}
extra := make(map[string]string)
if p.Metrics != nil {
extra["calls"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("calls", p.Metrics).RateMean())
extra["connections"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("connections", p.Metrics).RateMean())
}
// set this same metrics for all services at this server
for _, sm := range p.Services {
v, _ := url.ParseQuery(string(sm.Meta))
for key, value := range extra {
v.Set(key, value)
}
sm.Meta = v.Encode()
}
ss, _ := json.Marshal(p.Services)
s := url.QueryEscape(string(ss))
p.server.SetText([]string{s})
}
}
}()
}
return nil
}
// Stop unregister all services.
func (p *MDNSRegisterPlugin) Stop() error {
p.server.Shutdown()
close(p.dying)
<-p.done
return nil
}
func (p *MDNSRegisterPlugin) initMDNS() {
data, _ := json.Marshal(p.Services)
s := url.QueryEscape(string(data))
host, _ := os.Hostname()
addr := p.ServiceAddress
i := strings.Index(addr, "@")
if i > 0 {
addr = addr[i+1:]
}
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
p.port, err = strconv.Atoi(portStr)
if err != nil {
panic(err)
}
server, err := zeroconf.Register(host, "_rpcxservices", p.domain, p.port, []string{s}, nil)
if err != nil {
panic(err)
}
p.server = server
}
// HandleConnAccept handles connections from clients
func (p *MDNSRegisterPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
if p.Metrics != nil {
metrics.GetOrRegisterMeter("connections", p.Metrics).Mark(1)
}
return conn, true
}
// PreCall handles rpc call from clients
func (p *MDNSRegisterPlugin) PreCall(_ context.Context, _, _ string, args interface{}) (interface{}, error) {
if p.Metrics != nil {
metrics.GetOrRegisterMeter("calls", p.Metrics).Mark(1)
}
return args, nil
}
// Register handles registering event.
// this service is registered at BASE/serviceName/thisIpAddress node
func (p *MDNSRegisterPlugin) Register(name string, rcvr interface{}, metadata string) (err error) {
if strings.TrimSpace(name) == "" {
err = errors.New("Register service `name` can't be empty")
return
}
sm := &serviceMeta{
Service: name,
Meta: metadata,
ServiceAddress: p.ServiceAddress,
}
p.Services = append(p.Services, sm)
if p.server == nil {
p.initMDNS()
return
}
ss, _ := json.Marshal(p.Services)
s := url.QueryEscape(string(ss))
p.server.SetText([]string{s})
return
}
func (p *MDNSRegisterPlugin) RegisterFunction(serviceName, fname string, fn interface{}, metadata string) error {
return p.Register(serviceName, fn, metadata)
}
func (p *MDNSRegisterPlugin) Unregister(name string) (err error) {
if len(p.Services) == 0 {
return nil
}
if strings.TrimSpace(name) == "" {
err = errors.New("Register service `name` can't be empty")
return
}
var services = make([]*serviceMeta, 0, len(p.Services)-1)
for _, meta := range p.Services {
if meta.Service != name {
services = append(services, meta)
}
}
p.Services = services
ss, _ := json.Marshal(p.Services)
s := url.QueryEscape(string(ss))
p.server.SetText([]string{s})
// if p.server != nil {
// p.server.Shutdown()
// return
// }
return
}
================================================
FILE: serverplugin/metrics.go
================================================
package serverplugin
import (
"context"
"net"
"time"
"github.com/rcrowley/go-metrics"
"github.com/rcrowley/go-metrics/exp"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/server"
)
// MetricsPlugin has an issue. It changes seq of requests and it is wrong!!!!
// we should use other methods to map requests and responses not but seq.
// MetricsPlugin collects metrics of a rpc server.
// You can report metrics to log, syslog, Graphite, InfluxDB or others to display them in Dashboard such as grafana, Graphite.
type MetricsPlugin struct {
Registry metrics.Registry
Prefix string
}
// NewMetricsPlugin creates a new MetricsPlugin
func NewMetricsPlugin(registry metrics.Registry) *MetricsPlugin {
return &MetricsPlugin{Registry: registry}
}
func (p *MetricsPlugin) withPrefix(m string) string {
return p.Prefix + m
}
// Register handles registering event.
func (p *MetricsPlugin) Register(name string, rcvr interface{}, metadata string) error {
serviceCounter := metrics.GetOrRegisterCounter(p.withPrefix("serviceCounter"), p.Registry)
serviceCounter.Inc(1)
return nil
}
// HandleConnAccept handles connections from clients
func (p *MetricsPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
clientMeter := metrics.GetOrRegisterMeter(p.withPrefix("clientMeter"), p.Registry)
clientMeter.Mark(1)
return conn, true
}
// PreReadRequest marks start time of calling service
func (p *MetricsPlugin) PreReadRequest(ctx context.Context) error {
return nil
}
// PostReadRequest counts read
func (p *MetricsPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error {
sp := r.ServicePath
sm := r.ServiceMethod
if sp == "" {
return nil
}
m := metrics.GetOrRegisterMeter(p.withPrefix("service."+sp+"."+sm+".Read_Qps"), p.Registry)
m.Mark(1)
return nil
}
// PostWriteResponse count write
func (p *MetricsPlugin) PostWriteResponse(ctx context.Context, req *protocol.Message, res *protocol.Message, e error) error {
sp := res.ServicePath
sm := res.ServiceMethod
if sp == "" {
return nil
}
m := metrics.GetOrRegisterMeter(p.withPrefix("service."+sp+"."+sm+".Write_Qps"), p.Registry)
m.Mark(1)
t := ctx.Value(server.StartRequestContextKey).(int64)
if t > 0 {
t = time.Now().UnixNano() - t
if t < 30*time.Minute.Nanoseconds() { // it is impossible that calltime exceeds 30 minute
// Historgram
h := metrics.GetOrRegisterHistogram(p.withPrefix("service."+sp+"."+sm+".CallTime"), p.Registry,
metrics.NewExpDecaySample(1028, 0.015))
h.Update(t)
}
}
return nil
}
// Log reports metrics into logs.
//
// p.Log( 5 * time.Second, log.New(os.Stderr, "metrics: ", log.Lmicroseconds))
func (p *MetricsPlugin) Log(freq time.Duration, l metrics.Logger) {
go metrics.Log(p.Registry, freq, l)
}
// Graphite reports metrics into graphite.
//
// addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:2003")
// p.Graphite(10e9, "metrics", addr)
func (p *MetricsPlugin) Graphite(freq time.Duration, prefix string, addr *net.TCPAddr) {
go metrics.Graphite(p.Registry, freq, prefix, addr)
}
// // InfluxDB reports metrics into influxdb.
// //
// // p.InfluxDB(10e9, "http://127.0.0.1:8086","metrics", "test","test"})
// //
// func (p *MetricsPlugin) InfluxDB(freq time.Duration, url, database, username, password string) {
// go InfluxDB(p.Registry, freq, url, database, username, password)
// }
// // InfluxDBWithTags reports metrics into influxdb with tags.
// // you can set node info into tags.
// //
// // p.InfluxDBWithTags(10e9, "http://127.0.0.1:8086","metrics", "test","test", map[string]string{"host":"127.0.0.1"})
// //
// func (p *MetricsPlugin) InfluxDBWithTags(freq time.Duration, url, database, username, password string, tags map[string]string) {
// go InfluxDBWithTags(p.Registry, freq, url, database, username, password, tags)
// }
// Exp uses the same mechanism as the official expvar but exposed under /debug/metrics,
// which shows a json representation of all your usual expvars as well as all your go-metrics.
func (p *MetricsPlugin) Exp() {
exp.Exp(metrics.DefaultRegistry)
}
================================================
FILE: serverplugin/rate_limiting.go
================================================
package serverplugin
import (
"net"
"time"
"github.com/juju/ratelimit"
)
// RateLimitingPlugin can limit connecting per unit time
type RateLimitingPlugin struct {
FillInterval time.Duration
Capacity int64
bucket *ratelimit.Bucket
}
// NewRateLimitingPlugin creates a new RateLimitingPlugin
func NewRateLimitingPlugin(fillInterval time.Duration, capacity int64) *RateLimitingPlugin {
tb := ratelimit.NewBucket(fillInterval, capacity)
return &RateLimitingPlugin{
FillInterval: fillInterval,
Capacity: capacity,
bucket: tb}
}
// HandleConnAccept can limit connecting rate
func (plugin *RateLimitingPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
return conn, plugin.bucket.TakeAvailable(1) > 0
}
================================================
FILE: serverplugin/redis.go
================================================
package serverplugin
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"strings"
"sync"
"time"
metrics "github.com/rcrowley/go-metrics"
"github.com/rpcxio/libkv"
"github.com/rpcxio/libkv/store"
"github.com/rpcxio/libkv/store/redis"
"github.com/smallnest/rpcx/log"
)
func init() {
redis.Register()
}
// RedisRegisterPlugin implements redis registry.
type RedisRegisterPlugin struct {
// service address, for example, tcp@127.0.0.1:8972, quic@127.0.0.1:1234
ServiceAddress string
// redis addresses
RedisServers []string
// base path for rpcx server, for example com/example/rpcx
BasePath string
Metrics metrics.Registry
// Registered services
Services []string
metasLock sync.RWMutex
metas map[string]string
UpdateInterval time.Duration
Options *store.Config
kv store.Store
dying chan struct{}
done chan struct{}
}
// Start starts to connect redis cluster
func (p *RedisRegisterPlugin) Start() error {
if p.done == nil {
p.done = make(chan struct{})
}
if p.dying == nil {
p.dying = make(chan struct{})
}
if p.kv == nil {
kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options)
if err != nil {
log.Errorf("cannot create redis registry: %v", err)
close(p.done)
return err
}
p.kv = kv
}
err := p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true})
if err != nil && !strings.Contains(err.Error(), "Not a file") {
log.Errorf("cannot create redis path %s: %v", p.BasePath, err)
close(p.done)
return err
}
if p.UpdateInterval > 0 {
go func() {
ticker := time.NewTicker(p.UpdateInterval)
defer ticker.Stop()
defer p.kv.Close()
// refresh service TTL
for {
select {
case <-p.dying:
close(p.done)
return
case <-ticker.C:
extra := make(map[string]string)
if p.Metrics != nil {
extra["calls"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("calls", p.Metrics).RateMean())
extra["connections"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("connections", p.Metrics).RateMean())
}
// set this same metrics for all services at this server
for _, name := range p.Services {
nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
kvPair, err := p.kv.Get(nodePath)
if err != nil {
log.Infof("can't get data of node: %s, because of %v", nodePath, err.Error())
p.metasLock.RLock()
meta := p.metas[name]
p.metasLock.RUnlock()
err = p.kv.Put(nodePath, []byte(meta), &store.WriteOptions{TTL: p.UpdateInterval * 2})
if err != nil {
log.Errorf("cannot re-create redis path %s: %v", nodePath, err)
}
} else {
v, _ := url.ParseQuery(string(kvPair.Value))
for key, value := range extra {
v.Set(key, value)
}
p.kv.Put(nodePath, []byte(v.Encode()), &store.WriteOptions{TTL: p.UpdateInterval * 2})
}
}
}
}
}()
}
return nil
}
// Stop unregister all services.
func (p *RedisRegisterPlugin) Stop() error {
if p.kv == nil {
kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options)
if err != nil {
log.Errorf("cannot create redis registry: %v", err)
return err
}
p.kv = kv
}
for _, name := range p.Services {
nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
exist, err := p.kv.Exists(nodePath)
if err != nil {
log.Errorf("cannot delete path %s: %v", nodePath, err)
continue
}
if exist {
p.kv.Delete(nodePath)
log.Infof("delete path %s", nodePath, err)
}
}
close(p.dying)
<-p.done
return nil
}
// HandleConnAccept handles connections from clients
func (p *RedisRegisterPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
if p.Metrics != nil {
metrics.GetOrRegisterMeter("connections", p.Metrics).Mark(1)
}
return conn, true
}
// PreCall handles rpc call from clients
func (p *RedisRegisterPlugin) PreCall(_ context.Context, _, _ string, args interface{}) (interface{}, error) {
if p.Metrics != nil {
metrics.GetOrRegisterMeter("calls", p.Metrics).Mark(1)
}
return args, nil
}
// Register handles registering event.
// this service is registered at BASE/serviceName/thisIpAddress node
func (p *RedisRegisterPlugin) Register(name string, rcvr interface{}, metadata string) (err error) {
if strings.TrimSpace(name) == "" {
err = errors.New("Register service `name` can't be empty")
return
}
if p.kv == nil {
redis.Register()
kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options)
if err != nil {
log.Errorf("cannot create redis registry: %v", err)
return err
}
p.kv = kv
}
err = p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true})
if err != nil && !strings.Contains(err.Error(), "Not a file") {
log.Errorf("cannot create redis path %s: %v", p.BasePath, err)
return err
}
nodePath := fmt.Sprintf("%s/%s", p.BasePath, name)
err = p.kv.Put(nodePath, []byte(name), &store.WriteOptions{IsDir: true})
if err != nil && !strings.Contains(err.Error(), "Not a file") {
log.Errorf("cannot create redis path %s: %v", nodePath, err)
return err
}
nodePath = fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
err = p.kv.Put(nodePath, []byte(metadata), &store.WriteOptions{TTL: p.UpdateInterval * 2})
if err != nil {
log.Errorf("cannot create redis path %s: %v", nodePath, err)
return err
}
p.Services = append(p.Services, name)
p.metasLock.Lock()
if p.metas == nil {
p.metas = make(map[string]string)
}
p.metas[name] = metadata
p.metasLock.Unlock()
return
}
func (p *RedisRegisterPlugin) Unregister(name string) (err error) {
if len(p.Services) == 0 {
return nil
}
if strings.TrimSpace(name) == "" {
err = errors.New("Register service `name` can't be empty")
return
}
if p.kv == nil {
redis.Register()
kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options)
if err != nil {
log.Errorf("cannot create redis registry: %v", err)
return err
}
p.kv = kv
}
err = p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true})
if err != nil && !strings.Contains(err.Error(), "Not a file") {
log.Errorf("cannot create redis path %s: %v", p.BasePath, err)
return err
}
nodePath := fmt.Sprintf("%s/%s", p.BasePath, name)
err = p.kv.Put(nodePath, []byte(name), &store.WriteOptions{IsDir: true})
if err != nil && !strings.Contains(err.Error(), "Not a file") {
log.Errorf("cannot create redis path %s: %v", nodePath, err)
return err
}
nodePath = fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
err = p.kv.Delete(nodePath)
if err != nil {
log.Errorf("cannot remove redis path %s: %v", nodePath, err)
return err
}
var services = make([]string, 0, len(p.Services)-1)
for _, s := range p.Services {
if s != name {
services = append(services, s)
}
}
p.Services = services
p.metasLock.Lock()
if p.metas == nil {
p.metas = make(map[string]string)
}
delete(p.metas, name)
p.metasLock.Unlock()
return
}
================================================
FILE: serverplugin/registry_test.go
================================================
package serverplugin
import "context"
type Args struct {
A int
B int
}
type Reply struct {
C int
}
type Arith int
func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error {
reply.C = args.A * args.B
return nil
}
================================================
FILE: serverplugin/req_rate_limiting.go
================================================
package serverplugin
import (
"context"
"time"
"github.com/juju/ratelimit"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/server"
)
// ReqRateLimitingPlugin can limit requests per unit time
type ReqRateLimitingPlugin struct {
FillInterval time.Duration
Capacity int64
bucket *ratelimit.Bucket
block bool // blocks or return error if reach the limit
}
// NewReqRateLimitingPlugin creates a new RateLimitingPlugin
func NewReqRateLimitingPlugin(fillInterval time.Duration, capacity int64, block bool) *ReqRateLimitingPlugin {
tb := ratelimit.NewBucket(fillInterval, capacity)
return &ReqRateLimitingPlugin{
FillInterval: fillInterval,
Capacity: capacity,
bucket: tb,
block: block,
}
}
// PostReadRequest can limit request processing.
func (plugin *ReqRateLimitingPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error {
if plugin.block {
plugin.bucket.Wait(1)
return nil
}
count := plugin.bucket.TakeAvailable(1)
if count == 1 {
return nil
}
return server.ErrReqReachLimit
}
================================================
FILE: serverplugin/req_rate_limiting_redis.go
================================================
package serverplugin
import (
"context"
"time"
"github.com/go-redis/redis_rate/v10"
"github.com/redis/go-redis/v9"
"github.com/smallnest/rpcx/protocol"
"github.com/smallnest/rpcx/server"
)
var _ server.PostReadRequestPlugin = (*RedisRateLimitingPlugin)(nil)
// RedisRateLimitingPlugin can limit requests per unit time
type RedisRateLimitingPlugin struct {
addrs []string
limiter redis_rate.Limiter
limit redis_rate.Limit
}
// NewRedisRateLimitingPlugin creates a new RateLimitingPlugin
func NewRedisRateLimitingPlugin(addrs []string, rate int, burst int, period time.Duration) *RedisRateLimitingPlugin {
limit := redis_rate.Limit{
Rate: rate,
Burst: burst,
Period: period,
}
rdb := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: addrs,
})
limiter := redis_rate.NewLimiter(rdb)
return &RedisRateLimitingPlugin{
addrs: addrs,
limiter: *limiter,
limit: limit,
}
}
// PostReadRequest can limit request processing.
func (plugin *RedisRateLimitingPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error {
res, err := plugin.limiter.Allow(ctx, r.ServicePath+"/"+r.ServiceMethod, plugin.limit)
if err != nil {
return err
}
if res.Allowed > 0 {
return nil
}
return server.ErrReqReachLimit
}
================================================
FILE: serverplugin/tee.go
================================================
package serverplugin
import (
"io"
"net"
)
// TeeConnPlugin is a plugin that copy requests from clients and send to a io.Writer.
type TeeConnPlugin struct {
w io.Writer
}
func NewTeeConnPlugin(w io.Writer) *TeeConnPlugin {
return &TeeConnPlugin{w: w}
}
// Update can start a stream copy by setting a non-nil w.
// If you set a nil w, it doesn't copy stream.
func (plugin *TeeConnPlugin) Update(w io.Writer) {
plugin.w = w
}
// HandleConnAccept check ip.
func (plugin *TeeConnPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
tc := &teeConn{conn, plugin.w}
return tc, true
}
type teeConn struct {
net.Conn
w io.Writer
}
func (t *teeConn) Read(p []byte) (n int, err error) {
n, err = t.Conn.Read(p)
if n > 0 && t.w != nil {
t.w.Write(p[:n])
// if _, err := t.w.Write(p[:n]); err != nil {
// return n, err //discard error
// }
}
return
}
================================================
FILE: serverplugin/whitelist.go
================================================
package serverplugin
import "net"
// WhitelistPlugin is a plugin that control only ip addresses in whitelist can access services.
type WhitelistPlugin struct {
Whitelist map[string]bool
WhitelistMask []*net.IPNet // net.ParseCIDR("172.17.0.0/16") to get *net.IPNet
}
// HandleConnAccept check ip.
func (plugin *WhitelistPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
ip, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
return conn, false
}
if plugin.Whitelist[ip] {
return conn, true
}
remoteIP := net.ParseIP(ip)
for _, mask := range plugin.WhitelistMask {
if mask.Contains(remoteIP) {
return conn, true
}
}
return conn, false
}
================================================
FILE: share/context.go
================================================
package share
import (
"context"
"fmt"
"reflect"
"sync"
)
// var _ context.Context = &Context{}
// Context is a rpcx customized Context that can contains multiple values.
type Context struct {
tagsLock *sync.Mutex
tags map[interface{}]interface{}
context.Context
}
func NewContext(ctx context.Context) *Context {
tagsLock := &sync.Mutex{}
ctx = context.WithValue(ctx, ContextTagsLock, tagsLock)
return &Context{
tagsLock: tagsLock,
Context: ctx,
tags: map[interface{}]interface{}{isShareContext: true},
}
}
func (c *Context) Lock() {
c.tagsLock.Lock()
}
func (c *Context) Unlock() {
c.tagsLock.Unlock()
}
func (c *Context) Value(key interface{}) interface{} {
c.tagsLock.Lock()
defer c.tagsLock.Unlock()
if c.tags == nil {
c.tags = make(map[interface{}]interface{})
}
if v, ok := c.tags[key]; ok {
return v
}
return c.Context.Value(key)
}
func (c *Context) SetValue(key, val interface{}) {
c.tagsLock.Lock()
defer c.tagsLock.Unlock()
if c.tags == nil {
c.tags = make(map[interface{}]interface{})
}
c.tags[key] = val
}
// DeleteKey delete the kv pair by key.
func (c *Context) DeleteKey(key interface{}) {
c.tagsLock.Lock()
defer c.tagsLock.Unlock()
if c.tags == nil || key == nil {
return
}
delete(c.tags, key)
}
func (c *Context) String() string {
return fmt.Sprintf("%v.WithValue(%v)", c.Context, c.tags)
}
func WithValue(parent context.Context, key, val interface{}) *Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
tags := make(map[interface{}]interface{})
tags[key] = val
return &Context{Context: parent, tags: tags, tagsLock: &sync.Mutex{}}
}
func WithLocalValue(ctx *Context, key, val interface{}) *Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
if ctx.tags == nil {
ctx.tags = make(map[interface{}]interface{})
}
ctx.tags[key] = val
return ctx
}
// IsShareContext checks whether a context is share.Context.
func IsShareContext(ctx context.Context) bool {
ok := ctx.Value(isShareContext)
return ok != nil
}
================================================
FILE: share/context_test.go
================================================
package share
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
var (
TheAnswer = "Answer to the Ultimate Question of Life, the Universe, and Everything"
MagicNumber = 42
)
func TestContext(t *testing.T) {
rpcxContext := NewContext(context.Background())
assert.NotNil(t, rpcxContext.Context)
assert.NotNil(t, rpcxContext.tags)
rpcxContext.SetValue("string", TheAnswer)
rpcxContext.SetValue(42, MagicNumber)
assert.Equal(t, MagicNumber, rpcxContext.Value(42))
assert.Equal(t, TheAnswer, rpcxContext.Value("string"))
rpcxContext.SetValue("string", TheAnswer)
t.Log(rpcxContext.String())
}
func TestWithValue(t *testing.T) {
ctx := WithValue(context.Background(), "key", "value")
assert.NotNil(t, ctx.tags)
}
func TestWithLocalValue(t *testing.T) {
var c = NewContext(context.Background())
c.SetValue("key", "value")
ctx := WithLocalValue(c, "MagicNumber", "42")
assert.Equal(t, "value", ctx.tags["key"])
assert.Equal(t, "42", ctx.tags["MagicNumber"])
}
================================================
FILE: share/share.go
================================================
package share
import (
"github.com/smallnest/rpcx/codec"
"github.com/smallnest/rpcx/protocol"
)
const (
// DefaultRPCPath is used by ServeHTTP.
DefaultRPCPath = "/_rpcx_"
// AuthKey is used in metadata.
AuthKey = "__AUTH"
// ServerAddress is used to get address of the server by client
ServerAddress = "__ServerAddress"
// ServerTimeout timeout value passed from client to control timeout of server
ServerTimeout = "__ServerTimeout"
// SendFileServiceName is name of the file transfer service.
SendFileServiceName = "_filetransfer"
// StreamServiceName is name of the stream service.
StreamServiceName = "_streamservice"
// ContextTagsLock is name of the Context TagsLock.
ContextTagsLock = "_tagsLock"
// _isShareContext indicates this context is share.Contex.
isShareContext = "_isShareContext"
)
// Trace is a flag to write a trace log or not.
// You should not enable this flag for product environment and enable it only for test.
// It writes trace log with logger Debug level.
var Trace bool
// Codecs are codecs supported by rpcx. You can add customized codecs in Codecs.
var Codecs = map[protocol.SerializeType]codec.Codec{
protocol.SerializeNone: &codec.ByteCodec{},
protocol.JSON: &codec.JSONCodec{},
protocol.ProtoBuffer: &codec.PBCodec{},
protocol.MsgPack: &codec.MsgpackCodec{},
protocol.Thrift: &codec.ThriftCodec{},
}
// RegisterCodec register customized codec.
func RegisterCodec(t protocol.SerializeType, c codec.Codec) {
Codecs[t] = c
}
// ContextKey defines key type in context.
type ContextKey string
// ReqMetaDataKey is used to set metadata in context of requests.
var ReqMetaDataKey = ContextKey("__req_metadata")
// ResMetaDataKey is used to set metadata in context of responses.
var ResMetaDataKey = ContextKey("__res_metadata")
// FileTransferArgs args from clients.
type FileTransferArgs struct {
FileName string `json:"file_name,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
// Clone clones this DownloadFileArgs.
func (args FileTransferArgs) Clone() *FileTransferArgs {
meta := make(map[string]string)
for k, v := range args.Meta {
meta[k] = v
}
return &FileTransferArgs{
FileName: args.FileName,
FileSize: args.FileSize,
Meta: meta,
}
}
// FileTransferReply response to token and addr to clients.
type FileTransferReply struct {
Token []byte `json:"token,omitempty"`
Addr string `json:"addr,omitempty"`
}
// DownloadFileArgs args from clients.
type DownloadFileArgs struct {
FileName string `json:"file_name,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
// Clone clones this DownloadFileArgs.
func (args DownloadFileArgs) Clone() *DownloadFileArgs {
meta := make(map[string]string)
for k, v := range args.Meta {
meta[k] = v
}
return &DownloadFileArgs{
FileName: args.FileName,
Meta: meta,
}
}
// StreamServiceArgs is the request type for stream service.
type StreamServiceArgs struct {
Meta map[string]string `json:"meta,omitempty"`
}
// StreamServiceReply is the reply type for stream service.
type StreamServiceReply struct {
Token []byte `json:"token,omitempty"`
Addr string `json:"addr,omitempty"`
}
================================================
FILE: share/share_test.go
================================================
package share
import (
"testing"
"github.com/smallnest/rpcx/protocol"
"github.com/stretchr/testify/assert"
)
type MockCodec struct {}
func (codec MockCodec) Encode(i interface{}) ([]byte, error) {
return nil, nil
}
func (codec MockCodec) Decode(data []byte, i interface{}) error {
return nil
}
func TestShare(t *testing.T) {
registeredCodecNum := len(Codecs)
codec := MockCodec{}
mockCodecType := 127
RegisterCodec(protocol.SerializeType(mockCodecType), codec)
assert.Equal(t, registeredCodecNum + 1, len(Codecs))
}
================================================
FILE: tool/xgen/README.md
================================================
# xgen
`xgen` isn a tool that can help you generate a server stub for rpcx services.
It search structs in your specified files and add them as services. Currently it doesn't support registring functions.
## Usage
```sh
# install
go get -u github.com/smallnest/rpcx/tool/xgen/...
# run
xgen -o server.go .go
```
The above will generate server.go containing a rpcx which registers all exported struct types contained in `.go`.
## Options
```
-o string
specify the filename of the output
-pkg
process the whole package instead of just the given file
-r string
registry type. support etcd, consul, zookeeper, mdns (default "etcd")
-tags string
build tags to add to generated file
```
You can run as:
```sh
xgen [options] .go .go .go
```
for example, `xgen -o server.go a.go b.go /User/abc/go/src/github.com/abc/aaa/c.go`
or
```sh
xgen [options]
```
for example, `xgen -o server.go github.com/abc/aaa github.com/abc/bbb github.com/abc/ccc`
================================================
FILE: tool/xgen/parser/parser.go
================================================
// Package parser parses Go code and keeps track of all the types defined
// and provides access to all the constants defined for an int type.
package parser
import (
"go/ast"
"go/build"
"go/parser"
"go/token"
"unicode"
"unicode/utf8"
)
type Parser struct {
PkgPath string
PkgName string
PkgFullName string
StructNames map[string]bool
}
type visitor struct {
*Parser
name string
explicit bool
}
func isExported(name string) bool {
rune, _ := utf8.DecodeRuneInString(name)
return unicode.IsUpper(rune)
}
func (v *visitor) Visit(n ast.Node) (w ast.Visitor) {
switch n := n.(type) {
case *ast.Package:
return v
case *ast.File:
v.PkgName = n.Name.String()
return v
case *ast.GenDecl:
return v
case *ast.TypeSpec:
v.name = n.Name.String()
// Allow to specify non-structs explicitly independent of '-all' flag.
if v.explicit {
v.StructNames[v.name] = true
return nil
}
return v
case *ast.StructType:
return nil
case *ast.FuncDecl:
if isExported(v.name) {
if n.Type.Params.NumFields() == 3 && n.Type.Results.NumFields() == 1 {
params := n.Type.Params.List
result := n.Type.Results.List[0]
if result.Type.(*ast.Ident).Name != "error" {
return nil
}
if p, ok := params[0].Type.(*ast.SelectorExpr); ok {
x := p.X.(*ast.Ident)
s := p.Sel
if x.Name+"."+s.Name == "context.Context" {
v.StructNames[v.name] = true
}
}
}
}
return nil
}
return nil
}
func (p *Parser) Parse(fname string, isDir bool) error {
p.PkgPath = build.Default.GOPATH
fset := token.NewFileSet()
if isDir {
packages, err := parser.ParseDir(fset, fname, nil, parser.ParseComments)
if err != nil {
return err
}
for _, pckg := range packages {
ast.Walk(&visitor{Parser: p}, pckg)
}
} else {
f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments)
if err != nil {
return err
}
ast.Walk(&visitor{Parser: p}, f)
}
return nil
}
================================================
FILE: tool/xgen/xgen.go
================================================
package main
import (
"flag"
"fmt"
"go/build"
"io"
"log"
"os"
"path/filepath"
"github.com/smallnest/rpcx/tool/xgen/parser"
)
var (
processPkg = flag.Bool("pkg", false, "process the whole package instead of just the given file")
specifiedName = flag.String("o", "", "specify the filename of the output")
buildTags = flag.String("tags", "", "build tags to add to generated file")
registry = flag.String("r", "", "registry type. support etcd, consul, zookeeper, mdns")
)
func main() {
flag.Parse()
files := flag.Args()
if *processPkg {
if len(flag.Args()) == 0 {
log.Fatalf("not set packages")
}
var pkgFiles []string
for _, f := range files {
pkgFiles = append(pkgFiles, filepath.Join(filepath.Join(build.Default.GOPATH, "src", f)))
}
files = pkgFiles
}
if len(files) == 0 {
flag.Usage()
os.Exit(1)
}
var parsers []*parser.Parser
for _, fname := range files {
fInfo, err := os.Stat(fname)
if err != nil {
panic(err)
}
if !filepath.IsAbs(fname) {
fname, err = filepath.Abs(fname)
if err != nil {
panic(err)
}
}
relDir, err := filepath.Rel(filepath.Join(build.Default.GOPATH, "src"), fname)
if err != nil {
fmt.Printf("provided directory not under GOPATH (%s): %v",
build.Default.GOPATH, err)
return
}
p := &parser.Parser{PkgFullName: relDir, StructNames: make(map[string]bool)}
if err := p.Parse(fname, fInfo.IsDir()); err != nil {
fmt.Printf("Error parsing %v: %v", fname, err)
return
}
parsers = append(parsers, p)
_ = generate(parsers)
}
}
func generate(parsers []*parser.Parser) error {
var w io.Writer
if *specifiedName == "" {
w = os.Stdout
} else {
f, err := os.Create(*specifiedName)
if err != nil {
return err
}
defer f.Close()
w = f
}
if *buildTags != "" {
fmt.Fprintln(w, "// +build ", *buildTags)
fmt.Fprintln(w)
}
fmt.Fprintln(w, "// AUTOGENERATED FILE: rpcx server stub code")
fmt.Fprintln(w, "// compilable during generation.")
fmt.Fprintln(w)
fmt.Fprintln(w, "package main")
fmt.Fprintln(w)
fmt.Fprintln(w, "import (")
fmt.Fprintln(w, ` "flag"`)
fmt.Fprintln(w, ` "time"`)
fmt.Fprintln(w)
fmt.Fprintln(w, ` metrics "github.com/rcrowley/go-metrics"`)
fmt.Fprintln(w, ` "github.com/smallnest/rpcx/server"`)
fmt.Fprintln(w, ` "github.com/smallnest/rpcx/serverplugin"`)
var importedPackages = make(map[string]bool)
for _, p := range parsers {
if !importedPackages[p.PkgFullName] {
fmt.Fprintln(w, ` "`+p.PkgFullName+`"`)
importedPackages[p.PkgFullName] = true
}
}
fmt.Fprintln(w, ")")
fmt.Fprintln(w)
var mainFn = `
var (
addr = flag.String("addr", "localhost:8972", "server address")`
if *registry == "etcd" || *registry == "consul" || *registry == "zookeeper" {
mainFn = mainFn + `
rAddr = flag.String("rAddr", "localhost:2379", "register address")`
}
mainFn = mainFn + `
basePath = flag.String("base", "/rpcx", "prefix path")
)
func main() {
flag.Parse()
_ = time.Second
_ = metrics.UseNilMetrics
_ = serverplugin.GetFunctionName
s := server.NewServer()
addRegistryPlugin(s)
registerServices(s)
s.Serve("tcp", *addr)
}`
fmt.Fprintln(w, mainFn)
fmt.Fprintln(w, `func registerServices(s *server.Server) {`)
for _, p := range parsers {
for n := range p.StructNames {
fmt.Fprintln(w, ` s.Register(new(`+p.PkgName+"."+n+`), "")`)
}
}
fmt.Fprintln(w, `}`)
useRegistry := false
fmt.Fprintln(w, `func addRegistryPlugin(s *server.Server) {`)
switch *registry {
case "etcd":
fmt.Fprintln(w, ` // add registery
r := &serverplugin.EtcdRegisterPlugin{
ServiceAddress: "tcp@" + *addr,
EtcdServers: []string{*rAddr},
BasePath: *basePath,
Metrics: metrics.NewRegistry(),
UpdateInterval: time.Minute,
}`)
useRegistry = true
case "consul":
fmt.Fprintln(w, ` // add registery
r := &serverplugin.ConsulRegisterPlugin{
ServiceAddress: "tcp@" + *addr,
ConsulServers: []string{*rAddr},
BasePath: *basePath,
Metrics: metrics.NewRegistry(),
UpdateInterval: time.Minute,
}`)
useRegistry = true
case "zookeeper":
fmt.Fprintln(w, ` // add registery
r := &serverplugin.ZooKeeperRegisterPlugin{
ServiceAddress: "tcp@" + *addr,
ZooKeeperServers: []string{*rAddr},
BasePath: *basePath,
Metrics: metrics.NewRegistry(),
UpdateInterval: time.Minute,
}`)
useRegistry = true
case "mdns":
fmt.Fprintln(w, `
r := serverplugin.NewMDNSRegisterPlugin("tcp@"+*addr, 8972, metrics.NewRegistry(), time.Minute, "")`)
useRegistry = true
default:
fmt.Fprintln(w, `
`)
}
if useRegistry {
fmt.Fprintln(w, `
err := r.Start()
if err != nil {
//log.Fatal(err)
}
s.Plugins.Add(r)`)
}
fmt.Fprintln(w, `}`)
return nil
}
================================================
FILE: util/buffer_pool.go
================================================
package util
import (
"math"
"sync"
)
type levelPool struct {
size int
pool sync.Pool
}
func newLevelPool(size int) *levelPool {
return &levelPool{
size: size,
pool: sync.Pool{
New: func() interface{} {
data := make([]byte, size)
return &data
},
},
}
}
type LimitedPool struct {
minSize int
maxSize int
pools []*levelPool
}
func NewLimitedPool(minSize, maxSize int) *LimitedPool {
if maxSize < minSize {
panic("maxSize can't be less than minSize")
}
const multiplier = 2
var pools []*levelPool
curSize := minSize
for curSize < maxSize {
pools = append(pools, newLevelPool(curSize))
curSize *= multiplier
}
pools = append(pools, newLevelPool(maxSize))
return &LimitedPool{
minSize: minSize,
maxSize: maxSize,
pools: pools,
}
}
func (p *LimitedPool) findPool(size int) *levelPool {
if size > p.maxSize {
return nil
}
idx := int(math.Ceil(math.Log2(float64(size) / float64(p.minSize))))
if idx < 0 {
idx = 0
}
if idx > len(p.pools)-1 {
return nil
}
return p.pools[idx]
}
func (p *LimitedPool) findPutPool(size int) *levelPool {
if size > p.maxSize {
return nil
}
if size < p.minSize {
return nil
}
idx := int(math.Floor(math.Log2(float64(size) / float64(p.minSize))))
if idx < 0 {
idx = 0
}
if idx > len(p.pools)-1 {
return nil
}
return p.pools[idx]
}
func (p *LimitedPool) Get(size int) *[]byte {
sp := p.findPool(size)
if sp == nil {
data := make([]byte, size)
return &data
}
buf := sp.pool.Get().(*[]byte)
*buf = (*buf)[:size]
return buf
}
func (p *LimitedPool) Put(b *[]byte) {
sp := p.findPutPool(cap(*b))
if sp == nil {
return
}
*b = (*b)[:cap(*b)]
sp.pool.Put(b)
}
================================================
FILE: util/buffer_pool_test.go
================================================
package util
import (
"fmt"
"testing"
)
func TestLimitedPool_findPool(t *testing.T) {
pool := NewLimitedPool(512, 4096)
tests := []struct {
args int
want int
}{
{200, 512},
{512, 512},
{1000, 1024},
{2000, 2048},
{2048, 2048},
{4095, 4096},
{4096, 4096},
{4097, -1},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("bytes-%d", tt.args), func(t *testing.T) {
got := pool.findPool(tt.args)
if got == nil {
if tt.want > 0 {
fmt.Printf("expect %d pool but got nil", tt.want)
}
return
}
if got.size != tt.want {
fmt.Printf("expect %d pool but got %d pool", tt.want, got.size)
}
})
}
}
func TestLimitedPool_findPutPool(t *testing.T) {
pool := NewLimitedPool(512, 4096)
tests := []struct {
args int
want int
}{
{200, -1}, //too small so we discard it
{512, 512},
{1000, 512},
{2000, 1024},
{2048, 2048},
{4095, 2048},
{4096, 4096},
{4097, -1}, // too big so we discard it
}
for _, tt := range tests {
t.Run(fmt.Sprintf("bytes-%d", tt.args), func(t *testing.T) {
got := pool.findPutPool(tt.args)
if got == nil {
if tt.want > 0 {
fmt.Printf("expect %d pool but got nil", tt.want)
}
return
}
if got.size != tt.want {
fmt.Printf("expect %d pool but got %d pool", tt.want, got.size)
}
})
}
}
================================================
FILE: util/compress.go
================================================
package util
import (
"bytes"
"compress/gzip"
"io"
"sync"
)
var (
spWriter sync.Pool
spReader sync.Pool
spBuffer sync.Pool
)
func init() {
spWriter = sync.Pool{New: func() interface{} {
return gzip.NewWriter(nil)
}}
spReader = sync.Pool{New: func() interface{} {
return new(gzip.Reader)
}}
spBuffer = sync.Pool{New: func() interface{} {
return bytes.NewBuffer(nil)
}}
}
// Unzip unzips data.
func Unzip(data []byte) ([]byte, error) {
buf := bytes.NewBuffer(data)
gr := spReader.Get().(*gzip.Reader)
defer func() {
spReader.Put(gr)
}()
err := gr.Reset(buf)
if err != nil {
return nil, err
}
defer gr.Close()
data, err = io.ReadAll(gr)
if err != nil {
return nil, err
}
return data, err
}
// Zip zips data.
func Zip(data []byte) ([]byte, error) {
buf := spBuffer.Get().(*bytes.Buffer)
w := spWriter.Get().(*gzip.Writer)
w.Reset(buf)
defer func() {
buf.Reset()
spBuffer.Put(buf)
w.Close()
spWriter.Put(w)
}()
_, err := w.Write(data)
if err != nil {
return nil, err
}
err = w.Flush()
if err != nil {
return nil, err
}
err = w.Close()
if err != nil {
return nil, err
}
dec := buf.Bytes()
return dec, nil
}
================================================
FILE: util/compress_test.go
================================================
package util
import (
"bytes"
"compress/gzip"
"io"
"testing"
)
func TestZip(t *testing.T) {
s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D"
t.Logf("origin len: %d", len(s))
data, err := Zip([]byte(s))
if err != nil {
t.Fatalf("failed to zip: %v", err)
}
t.Logf("zipped len: %d", len(data))
s2, err := Unzip(data)
if err != nil {
t.Fatalf("failed to unzip: %v", err)
}
if string(s2) != s {
t.Fatalf("unzip data is wrong")
}
}
func BenchmarkZip(b *testing.B) {
s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D"
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, err := Zip([]byte(s))
if err != nil {
b.Errorf("failed to zip: %v", err)
}
_ = data
}
})
}
func BenchmarkUnzip(b *testing.B) {
s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D"
data, err := Zip([]byte(s))
if err != nil {
b.Fatalf("failed to zip: %v", err)
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
s2, err := Unzip(data)
if err != nil {
b.Errorf("failed to zip: %v", err)
}
_ = s2
}
})
}
func oldUnzip(data []byte) ([]byte, error) {
buf := spBuffer.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
spBuffer.Put(buf)
}()
_, err := buf.Write(data)
if err != nil {
return nil, err
}
gr := spReader.Get().(*gzip.Reader)
defer func() {
spReader.Put(gr)
}()
err = gr.Reset(buf)
if err != nil {
return nil, err
}
defer gr.Close()
data, err = io.ReadAll(gr)
if err != nil {
return nil, err
}
return data, err
}
func BenchmarkUnzip_Old(b *testing.B) {
s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D"
data, err := Zip([]byte(s))
if err != nil {
b.Fatalf("failed to zip: %v", err)
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
s2, err := oldUnzip(data)
if err != nil {
b.Errorf("failed to zip: %v", err)
}
_ = s2
}
})
}
================================================
FILE: util/converter.go
================================================
package util
import (
"unsafe"
)
func SliceByteToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func StringToSliceByte(s string) []byte {
x := (*[2]uintptr)(unsafe.Pointer(&s))
h := [3]uintptr{x[0], x[1], x[1]}
return *(*[]byte)(unsafe.Pointer(&h))
}
func CopyMeta(src, dst map[string]string) {
if dst == nil {
return
}
for k, v := range src {
dst[k] = v
}
}
================================================
FILE: util/net.go
================================================
package util
import (
"errors"
"fmt"
"net"
"net/url"
"sort"
"strconv"
"strings"
)
// GetFreePort gets a free port.
func GetFreePort() (port int, err error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, err
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port, nil
}
// ParseRpcxAddress parses rpcx address such as tcp@127.0.0.1:8972 quic@192.168.1.1:9981
func ParseRpcxAddress(addr string) (network string, ip string, port int, err error) {
ati := strings.Index(addr, "@")
if ati <= 0 {
return "", "", 0, fmt.Errorf("invalid rpcx address: %s", addr)
}
network = addr[:ati]
addr = addr[ati+1:]
var portstr string
ip, portstr, err = net.SplitHostPort(addr)
if err != nil {
return "", "", 0, err
}
port, err = strconv.Atoi(portstr)
return network, ip, port, err
}
func ConvertMeta2Map(meta string) map[string]string {
var rt = make(map[string]string)
if meta == "" {
return rt
}
v, err := url.ParseQuery(meta)
if err != nil {
return rt
}
for key := range v {
rt[key] = v.Get(key)
}
return rt
}
func ConvertMap2String(meta map[string]string) string {
var buf strings.Builder
keys := make([]string, 0, len(meta))
for k := range meta {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
vs := meta[k]
keyEscaped := url.QueryEscape(k)
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(keyEscaped)
buf.WriteByte('=')
buf.WriteString(url.QueryEscape(vs))
}
return buf.String()
}
// ExternalIPV4 gets external IPv4 address of this server.
func ExternalIPV4() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue // not an ipv4 address
}
return ip.String(), nil
}
}
return "", errors.New("are you connected to the network?")
}
// ExternalIPV6 gets external IPv6 address of this server.
func ExternalIPV6() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To16()
if ip == nil {
continue // not an ipv4 address
}
return ip.String(), nil
}
}
return "", errors.New("are you connected to the network?")
}
================================================
FILE: util/net_test.go
================================================
package util
import (
"net"
"strconv"
"testing"
)
func TestGetFreePort(t *testing.T) {
for i := 0; i < 1000; i++ {
port, err := GetFreePort()
if err != nil {
t.Error(err)
}
if port == 0 {
t.Error("GetFreePort() return 0")
}
}
}
var oldGetFreePort = func() (port int, err error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, err
}
defer listener.Close()
addr := listener.Addr().String()
_, portString, err := net.SplitHostPort(addr)
if err != nil {
return 0, err
}
return strconv.Atoi(portString)
}
func BenchmarkGetFreePort_Old(b *testing.B) {
for i := 0; i < b.N; i++ {
oldGetFreePort()
}
}
func BenchmarkGetFreePort_New(b *testing.B) {
for i := 0; i < b.N; i++ {
GetFreePort()
}
}
func TestExternalIPV4(t *testing.T) {
ip, err := ExternalIPV4()
if err != nil {
t.Fatal(err)
}
if ip == "" {
t.Fatal("expect an IP but got empty")
}
t.Log(ip)
}
func TestExternalIPV6(t *testing.T) {
ip, err := ExternalIPV6()
if err != nil {
t.Fatal(err)
}
if ip == "" {
t.Fatal("expect an IP but got empty")
}
t.Log(ip)
}