move options to dedicated package
Some checks failed
lint / lint (pull_request) Failing after 1m31s
pr / test (pull_request) Failing after 2m37s

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
2023-07-29 00:40:58 +03:00
parent b1dbd99ce2
commit 6f6f850af6
84 changed files with 1154 additions and 4521 deletions

63
util/ctx/ctx.go Normal file
View File

@@ -0,0 +1,63 @@
package ctx
import "context"
// Mixin mixes a typically shorter-lived (service, etc.) context passed in
// “shortctx” into a long-living context “longctx”, returning a derived “mixed”
// context. The “mixed” context is derived from the long-living context and
// additionally gets the deadline (if any) and cancellation of the mixed-in
// “shortctx”.
//
// Please note that it is essential to always call the additionally returned
// cancel function in order to not leak go routines. Calling this cancel
// function won't cancel the long-living and short-lived contexts, just clean
// up. This follows the established context pattern of [context.WithCancel],
// [context.WithDeadline] and [context.WithTimeout].
func Mixin(longctx, shortctx context.Context) (mixedctx context.Context, mixedcancel context.CancelFunc) {
if longctx == nil {
panic("wye.Mixin: cannot mix into nil context")
}
if shortctx == nil {
panic("wye.Mixin: cannot mix-in nil context")
}
mixedctx = longctx
shortDone := shortctx.Done()
if shortDone == nil {
// In case the shorter-living context isn't cancellable at all, then we
// cannot cancel it and the cancel function returned must be a "no
// operation". There's no need to mix in something, so we can pass on
// the long-lived context.
mixedcancel = func() {}
return
}
// Nota bene: cancelled contexts "trickle down", so if a context higher up
// the hierarchy was cancelled, this will automatically propagate down to
// all child contexts, and so on.
//
// In case the shorter-lived context has a deadline, we need to carry it
// over into the final mixed context.
if deadline, ok := shortctx.Deadline(); ok {
mixedctx, mixedcancel = context.WithDeadline(mixedctx, deadline)
}
// As the shorter-living context can be cancelled, we will need to supervise
// it so we notice when it gets cancelled and then cancel the mixed context.
mixedctx, mixedcancel = context.WithCancel(mixedctx)
go func(ctx context.Context, cancel context.CancelFunc) {
select {
case <-shortDone:
// In case the shorter-living context was cancelled (but it did
// not pass a deadline) then we need to propagate this mixed
// context. Please note this correctly won't cancel the original
// longer context, because that's a long-living context we
// shouldn't interfere with.
if shortctx.Err() == context.Canceled {
cancel()
}
case <-ctx.Done():
// The final mixed context was either cancelled itself or its parent
// deadline context met its fate; here, do not touch short-lived
// context.
}
}(mixedctx, mixedcancel)
return
}

View File

@@ -1,40 +0,0 @@
// Package io is for io management
package io // import "go.unistack.org/micro/v4/util/io"
import (
"io"
"go.unistack.org/micro/v4/network/transport"
)
type rwc struct {
socket transport.Socket
}
func (r *rwc) Read(p []byte) (n int, err error) {
m := new(transport.Message)
if err := r.socket.Recv(m); err != nil {
return 0, err
}
copy(p, m.Body)
return len(m.Body), nil
}
func (r *rwc) Write(p []byte) (n int, err error) {
err = r.socket.Send(&transport.Message{
Body: p,
})
if err != nil {
return 0, err
}
return len(p), nil
}
func (r *rwc) Close() error {
return r.socket.Close()
}
// NewRWC returns a new ReadWriteCloser
func NewRWC(sock transport.Socket) io.ReadWriteCloser {
return &rwc{sock}
}

View File

@@ -1,118 +0,0 @@
package pool
import (
"context"
"sync"
"time"
"go.unistack.org/micro/v4/network/transport"
"go.unistack.org/micro/v4/util/id"
)
type pool struct {
tr transport.Transport
conns map[string][]*poolConn
size int
ttl time.Duration
sync.Mutex
}
type poolConn struct {
created time.Time
transport.Client
id string
}
func newPool(options Options) *pool {
return &pool{
size: options.Size,
tr: options.Transport,
ttl: options.TTL,
conns: make(map[string][]*poolConn),
}
}
func (p *pool) Close() error {
p.Lock()
for k, c := range p.conns {
for _, conn := range c {
conn.Client.Close()
}
delete(p.conns, k)
}
p.Unlock()
return nil
}
// NoOp the Close since we manage it
func (p *poolConn) Close() error {
return nil
}
func (p *poolConn) ID() string {
return p.id
}
func (p *poolConn) Created() time.Time {
return p.created
}
func (p *pool) Get(ctx context.Context, addr string, opts ...transport.DialOption) (Conn, error) {
p.Lock()
conns := p.conns[addr]
// while we have conns check age and then return one
// otherwise we'll create a new conn
for len(conns) > 0 {
conn := conns[len(conns)-1]
conns = conns[:len(conns)-1]
p.conns[addr] = conns
// if conn is old kill it and move on
if d := time.Since(conn.Created()); d > p.ttl {
conn.Client.Close()
continue
}
// we got a good conn, lets unlock and return it
p.Unlock()
return conn, nil
}
p.Unlock()
// create new conn
c, err := p.tr.Dial(ctx, addr, opts...)
if err != nil {
return nil, err
}
id, err := id.New()
if err != nil {
return nil, err
}
return &poolConn{
Client: c,
id: id,
created: time.Now(),
}, nil
}
func (p *pool) Release(conn Conn, err error) error {
// don't store the conn if it has errored
if err != nil {
return conn.(*poolConn).Client.Close()
}
// otherwise put it back for reuse
p.Lock()
conns := p.conns[conn.Remote()]
if len(conns) >= p.size {
p.Unlock()
return conn.(*poolConn).Client.Close()
}
p.conns[conn.Remote()] = append(conns, conn.(*poolConn))
p.Unlock()
return nil
}

View File

@@ -1,92 +0,0 @@
//go:build ignore
// +build ignore
package pool
import (
"testing"
"time"
"go.unistack.org/micro/v4/network/transport"
"go.unistack.org/micro/v4/network/transport/memory"
)
func testPool(t *testing.T, size int, ttl time.Duration) {
// mock transport
tr := memory.NewTransport()
options := Options{
TTL: ttl,
Size: size,
Transport: tr,
}
// zero pool
p := newPool(options)
// listen
l, err := tr.Listen(":0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
// accept loop
go func() {
for {
if err := l.Accept(func(s transport.Socket) {
for {
var msg transport.Message
if err := s.Recv(&msg); err != nil {
return
}
if err := s.Send(&msg); err != nil {
return
}
}
}); err != nil {
return
}
}
}()
for i := 0; i < 10; i++ {
// get a conn
c, err := p.Get(l.Addr())
if err != nil {
t.Fatal(err)
}
msg := &transport.Message{
Body: []byte(`hello world`),
}
if err := c.Send(msg); err != nil {
t.Fatal(err)
}
var rcv transport.Message
if err := c.Recv(&rcv); err != nil {
t.Fatal(err)
}
if string(rcv.Body) != string(msg.Body) {
t.Fatalf("got %v, expected %v", rcv.Body, msg.Body)
}
// release the conn
p.Release(c, nil)
p.Lock()
if i := len(p.conns[l.Addr()]); i > size {
p.Unlock()
t.Fatalf("pool size %d is greater than expected %d", i, size)
}
p.Unlock()
}
}
func TestClientPool(t *testing.T) {
testPool(t, 0, time.Minute)
testPool(t, 2, time.Minute)
}

View File

@@ -1,38 +0,0 @@
package pool
import (
"time"
"go.unistack.org/micro/v4/network/transport"
)
// Options struct
type Options struct {
Transport transport.Transport
TTL time.Duration
Size int
}
// Option func signature
type Option func(*Options)
// Size sets the size
func Size(i int) Option {
return func(o *Options) {
o.Size = i
}
}
// Transport sets the transport
func Transport(t transport.Transport) Option {
return func(o *Options) {
o.Transport = t
}
}
// TTL specifies ttl
func TTL(t time.Duration) Option {
return func(o *Options) {
o.TTL = t
}
}

View File

@@ -1,38 +0,0 @@
// Package pool is a connection pool
package pool // import "go.unistack.org/micro/v4/util/pool"
import (
"context"
"time"
"go.unistack.org/micro/v4/network/transport"
)
// Pool is an interface for connection pooling
type Pool interface {
// Close the pool
Close() error
// Get a connection
Get(ctx context.Context, addr string, opts ...transport.DialOption) (Conn, error)
// Release the connection
Release(c Conn, status error) error
}
// Conn conn pool interface
type Conn interface {
// unique id of connection
ID() string
// time it was created
Created() time.Time
// embedded connection
transport.Client
}
// NewPool creates new connection pool
func NewPool(opts ...Option) Pool {
options := Options{}
for _, o := range opts {
o(&options)
}
return newPool(options)
}

View File

@@ -76,6 +76,9 @@ func ZeroFieldByPath(src interface{}, path string) error {
val := reflect.ValueOf(src)
for _, p := range strings.Split(path, ".") {
if p == "" {
continue
}
val, err = structValueByName(val, p)
if err != nil {
return err
@@ -101,6 +104,9 @@ func SetFieldByPath(src interface{}, dst interface{}, path string) error {
val := reflect.ValueOf(src)
for _, p := range strings.Split(path, ".") {
if p == "" {
continue
}
val, err = structValueByName(val, p)
if err != nil {
return err
@@ -111,7 +117,20 @@ func SetFieldByPath(src interface{}, dst interface{}, path string) error {
return ErrInvalidStruct
}
val.Set(reflect.ValueOf(dst))
nv := reflect.ValueOf(dst)
if val.Kind() != nv.Kind() {
switch {
default:
val.Set(nv)
case nv.Kind() == reflect.Slice:
val.Set(nv.Index(0))
case val.Kind() == reflect.Slice:
val.Set(reflect.MakeSlice(val.Type(), 1, 1))
val.Index(0).Set(nv)
}
} else {
val.Set(nv)
}
return nil
}
@@ -157,6 +176,9 @@ func structValueByName(sv reflect.Value, tkey string) (reflect.Value, error) {
func StructFieldByPath(src interface{}, path string) (interface{}, error) {
var err error
for _, p := range strings.Split(path, ".") {
if p == "" {
continue
}
src, err = StructFieldByName(src, p)
if err != nil {
return nil, err

View File

@@ -62,6 +62,42 @@ func TestStructFieldsNested(t *testing.T) {
}
}
func TestSetFieldByPathMultiple(t *testing.T) {
var err error
tv := "test_val"
type Str1 struct {
Name []string `json:"name"`
}
val1 := &Str1{}
err = rutil.SetFieldByPath(val1, tv, ".Name")
if err != nil {
t.Fatal(err)
}
if len(val1.Name) != 1 {
t.Fatal("assign error")
} else if val1.Name[0] != tv {
t.Fatal("assign error")
}
type Str2 struct {
Name string `json:"name"`
}
val2 := &Str2{}
err = rutil.SetFieldByPath(val2, []string{tv}, ".Name")
if err != nil {
t.Fatal(err)
}
if len(val1.Name) != 1 {
t.Fatal("assign error")
} else if val1.Name[0] != tv {
t.Fatal("assign error")
}
}
func TestSetFieldByPath(t *testing.T) {
type NestedStr struct {
BBB string `json:"bbb"`

View File

@@ -1,65 +0,0 @@
package socket
import (
"sync"
)
// Pool holds the socket pool
type Pool struct {
sync.RWMutex
pool map[string]*Socket
}
// Get socket from pool
func (p *Pool) Get(id string) (*Socket, bool) {
// attempt to get existing socket
p.RLock()
socket, ok := p.pool[id]
if ok {
p.RUnlock()
return socket, ok
}
p.RUnlock()
// save socket
p.Lock()
defer p.Unlock()
// double checked locking
socket, ok = p.pool[id]
if ok {
return socket, ok
}
// create new socket
socket = New(id)
p.pool[id] = socket
// return socket
return socket, false
}
// Release close the socket and delete from pool
func (p *Pool) Release(s *Socket) {
p.Lock()
defer p.Unlock()
// close the socket
s.Close()
delete(p.pool, s.id)
}
// Close the pool and delete all the sockets
func (p *Pool) Close() {
p.Lock()
defer p.Unlock()
for id, sock := range p.pool {
sock.Close()
delete(p.pool, id)
}
}
// NewPool returns a new socket pool
func NewPool() *Pool {
return &Pool{
pool: make(map[string]*Socket),
}
}

View File

@@ -1,118 +0,0 @@
// Package socket provides a pseudo socket
package socket // import "go.unistack.org/micro/v4/util/socket"
import (
"io"
"go.unistack.org/micro/v4/network/transport"
)
// Socket is our pseudo socket for transport.Socket
type Socket struct {
closed chan bool
send chan *transport.Message
recv chan *transport.Message
id string
remote string
local string
}
// SetLocal sets the local addr
func (s *Socket) SetLocal(l string) {
s.local = l
}
// SetRemote sets the remote addr
func (s *Socket) SetRemote(r string) {
s.remote = r
}
// Accept passes a message to the socket which will be processed by the call to Recv
func (s *Socket) Accept(m *transport.Message) error {
select {
case s.recv <- m:
return nil
case <-s.closed:
return io.EOF
}
}
// Process takes the next message off the send queue created by a call to Send
func (s *Socket) Process(m *transport.Message) error {
select {
case msg := <-s.send:
*m = *msg
case <-s.closed:
// see if we need to drain
select {
case msg := <-s.send:
*m = *msg
return nil
default:
return io.EOF
}
}
return nil
}
// Remote returns remote addr
func (s *Socket) Remote() string {
return s.remote
}
// Local returns local addr
func (s *Socket) Local() string {
return s.local
}
// Send message by via transport
func (s *Socket) Send(m *transport.Message) error {
// send a message
select {
case s.send <- m:
case <-s.closed:
return io.EOF
}
return nil
}
// Recv message from transport
func (s *Socket) Recv(m *transport.Message) error {
// receive a message
select {
case msg := <-s.recv:
// set message
*m = *msg
case <-s.closed:
return io.EOF
}
// return nil
return nil
}
// Close closes the socket
func (s *Socket) Close() error {
select {
case <-s.closed:
// no op
default:
close(s.closed)
}
return nil
}
// New returns a new pseudo socket which can be used in the place of a transport socket.
// Messages are sent to the socket via Accept and receives from the socket via Process.
// SetLocal/SetRemote should be called before using the socket.
func New(id string) *Socket {
return &Socket{
id: id,
closed: make(chan bool),
local: "local",
remote: "remote",
send: make(chan *transport.Message, 128),
recv: make(chan *transport.Message, 128),
}
}