Compare commits
2 Commits
v3.11.47
...
4de9431df0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4de9431df0 | |||
| 19e3288b1d |
@@ -1,5 +1,5 @@
|
|||||||
# Micro
|
# Micro
|
||||||

|

|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
[](https://pkg.go.dev/go.unistack.org/micro/v3?tab=overview)
|
[](https://pkg.go.dev/go.unistack.org/micro/v3?tab=overview)
|
||||||
[](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av3+event%3Apush)
|
[](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av3+event%3Apush)
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import (
|
|||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultCodecs will be used to encode/decode data
|
||||||
|
var DefaultCodecs = map[string]codec.Codec{
|
||||||
|
"application/octet-stream": codec.NewCodec(),
|
||||||
|
}
|
||||||
|
|
||||||
type noopClient struct {
|
type noopClient struct {
|
||||||
funcPublish FuncPublish
|
funcPublish FuncPublish
|
||||||
funcBatchPublish FuncBatchPublish
|
funcBatchPublish FuncBatchPublish
|
||||||
@@ -173,6 +178,9 @@ func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
|
|||||||
if cf, ok := n.opts.Codecs[contentType]; ok {
|
if cf, ok := n.opts.Codecs[contentType]; ok {
|
||||||
return cf, nil
|
return cf, nil
|
||||||
}
|
}
|
||||||
|
if cf, ok := DefaultCodecs[contentType]; ok {
|
||||||
|
return cf, nil
|
||||||
|
}
|
||||||
return nil, codec.ErrUnknownContentType
|
return nil, codec.ErrUnknownContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/codec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type testHook struct {
|
type testHook struct {
|
||||||
@@ -21,7 +19,7 @@ func (t *testHook) Publish(fn FuncPublish) FuncPublish {
|
|||||||
func TestNoopHook(t *testing.T) {
|
func TestNoopHook(t *testing.T) {
|
||||||
h := &testHook{}
|
h := &testHook{}
|
||||||
|
|
||||||
c := NewClient(Codec("application/octet-stream", codec.NewCodec()), Hooks(HookPublish(h.Publish)))
|
c := NewClient(Hooks(HookPublish(h.Publish)))
|
||||||
|
|
||||||
if err := c.Init(); err != nil {
|
if err := c.Init(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
options := Options{
|
options := Options{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
ContentType: DefaultContentType,
|
ContentType: DefaultContentType,
|
||||||
Codecs: make(map[string]codec.Codec),
|
Codecs: DefaultCodecs,
|
||||||
CallOptions: CallOptions{
|
CallOptions: CallOptions{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Backoff: DefaultBackoff,
|
Backoff: DefaultBackoff,
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newSQLRowError() *sql.Row {
|
|
||||||
row := &sql.Row{}
|
|
||||||
t := reflect.TypeOf(row).Elem()
|
|
||||||
field, _ := t.FieldByName("err")
|
|
||||||
rowPtr := unsafe.Pointer(row)
|
|
||||||
errFieldPtr := unsafe.Pointer(uintptr(rowPtr) + field.Offset)
|
|
||||||
errPtr := (*error)(errFieldPtr)
|
|
||||||
*errPtr = ErrorNoAliveNodes
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClusterQuerier interface {
|
|
||||||
Querier
|
|
||||||
WaitForNodes(ctx context.Context, criterion ...hasql.NodeStateCriterion) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
hasql *hasql.Cluster[Querier]
|
|
||||||
options ClusterOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCluster returns [Querier] that provides cluster of nodes
|
|
||||||
func NewCluster[T Querier](opts ...ClusterOption) (ClusterQuerier, error) {
|
|
||||||
options := ClusterOptions{Context: context.Background()}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&options)
|
|
||||||
}
|
|
||||||
if options.NodeChecker == nil {
|
|
||||||
return nil, ErrClusterChecker
|
|
||||||
}
|
|
||||||
if options.NodeDiscoverer == nil {
|
|
||||||
return nil, ErrClusterDiscoverer
|
|
||||||
}
|
|
||||||
if options.NodePicker == nil {
|
|
||||||
return nil, ErrClusterPicker
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.Retries < 1 {
|
|
||||||
options.Retries = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.NodeStateCriterion == 0 {
|
|
||||||
options.NodeStateCriterion = hasql.Primary
|
|
||||||
}
|
|
||||||
|
|
||||||
options.Options = append(options.Options, hasql.WithNodePicker(options.NodePicker))
|
|
||||||
if p, ok := options.NodePicker.(*CustomPicker[Querier]); ok {
|
|
||||||
p.opts.Priority = options.NodePriority
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := hasql.NewCluster(
|
|
||||||
options.NodeDiscoverer,
|
|
||||||
options.NodeChecker,
|
|
||||||
options.Options...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Cluster{hasql: c, options: options}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) {
|
|
||||||
var tx *sql.Tx
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if tx, err = n.DB().BeginTx(ctx, opts); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if tx == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) Close() error {
|
|
||||||
return c.hasql.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) Conn(ctx context.Context) (*sql.Conn, error) {
|
|
||||||
var conn *sql.Conn
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if conn, err = n.DB().Conn(ctx); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if conn == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
|
||||||
var res sql.Result
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if res, err = n.DB().ExecContext(ctx, query, args...); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
|
|
||||||
var res *sql.Stmt
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if res, err = n.DB().PrepareContext(ctx, query); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
|
|
||||||
var res *sql.Rows
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if res, err = n.DB().QueryContext(ctx, query); err != nil && err != sql.ErrNoRows && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
|
|
||||||
var res *sql.Row
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
res = n.DB().QueryRowContext(ctx, query, args...)
|
|
||||||
if res.Err() == nil {
|
|
||||||
return false
|
|
||||||
} else if res.Err() != nil && retries >= c.options.Retries {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil {
|
|
||||||
res = newSQLRowError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) PingContext(ctx context.Context) error {
|
|
||||||
var err error
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
ok = true
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if err = n.DB().PingContext(ctx); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) WaitForNodes(ctx context.Context, criterions ...hasql.NodeStateCriterion) error {
|
|
||||||
for _, criterion := range criterions {
|
|
||||||
if _, err := c.hasql.WaitForNode(ctx, criterion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewCluster(t *testing.T) {
|
|
||||||
dbMaster, dbMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbMaster.Close()
|
|
||||||
dbMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(1, 0)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("master-dc1"))
|
|
||||||
|
|
||||||
dbDRMaster, dbDRMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbDRMaster.Close()
|
|
||||||
dbDRMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 40)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster1-dc2"))
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster"))
|
|
||||||
|
|
||||||
dbSlaveDC1, dbSlaveDC1Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC1.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
dbSlaveDC2, dbSlaveDC2Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC2.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
tctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
c, err := NewCluster[Querier](
|
|
||||||
WithClusterContext(tctx),
|
|
||||||
WithClusterNodeChecker(hasql.PostgreSQLChecker),
|
|
||||||
WithClusterNodePicker(NewCustomPicker[Querier](
|
|
||||||
CustomPickerMaxLag(100),
|
|
||||||
)),
|
|
||||||
WithClusterNodes(
|
|
||||||
ClusterNode{"slave-dc1", dbSlaveDC1, 1},
|
|
||||||
ClusterNode{"master-dc1", dbMaster, 1},
|
|
||||||
ClusterNode{"slave-dc2", dbSlaveDC2, 2},
|
|
||||||
ClusterNode{"drmaster1-dc2", dbDRMaster, 0},
|
|
||||||
),
|
|
||||||
WithClusterOptions(
|
|
||||||
hasql.WithUpdateInterval[Querier](2*time.Second),
|
|
||||||
hasql.WithUpdateTimeout[Querier](1*time.Second),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if err = c.WaitForNodes(tctx, hasql.Primary, hasql.Standby); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
node1Name := ""
|
|
||||||
fmt.Printf("check for Standby\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.Standby), "SELECT node_name as name"); row.Err() != nil {
|
|
||||||
t.Fatal(row.Err())
|
|
||||||
} else if err = row.Scan(&node1Name); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if "slave-dc1" != node1Name {
|
|
||||||
t.Fatalf("invalid node name %s != %s", "slave-dc1", node1Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
node2Name := ""
|
|
||||||
fmt.Printf("check for PreferStandby\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferStandby), "SELECT node_name as name"); row.Err() != nil {
|
|
||||||
t.Fatal(row.Err())
|
|
||||||
} else if err = row.Scan(&node2Name); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if "slave-dc1" != node2Name {
|
|
||||||
t.Fatalf("invalid node name %s != %s", "slave-dc1", node2Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
node3Name := ""
|
|
||||||
fmt.Printf("check for PreferPrimary\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferPrimary), "SELECT node_name as name"); row.Err() != nil {
|
|
||||||
t.Fatal(row.Err())
|
|
||||||
} else if err = row.Scan(&node3Name); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if "master-dc1" != node3Name {
|
|
||||||
t.Fatalf("invalid node name %s != %s", "master-dc1", node3Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`.*`).WillReturnRows(sqlmock.NewRows([]string{"role"}).RowError(1, fmt.Errorf("row error")))
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
fmt.Printf("check for PreferStandby\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferStandby), "SELECT node_name as name"); row.Err() == nil {
|
|
||||||
t.Fatal("must return error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbMasterErr := dbMasterMock.ExpectationsWereMet(); dbMasterErr != nil {
|
|
||||||
t.Error(dbMasterErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Querier interface {
|
|
||||||
// Basic connection methods
|
|
||||||
PingContext(ctx context.Context) error
|
|
||||||
Close() error
|
|
||||||
|
|
||||||
// Query methods with context
|
|
||||||
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
|
||||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
|
||||||
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
|
||||||
|
|
||||||
// Prepared statements with context
|
|
||||||
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
|
||||||
|
|
||||||
// Transaction management with context
|
|
||||||
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
|
|
||||||
|
|
||||||
Conn(ctx context.Context) (*sql.Conn, error)
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"database/sql/driver"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenDBWithCluster creates a [*sql.DB] that uses the [ClusterQuerier]
|
|
||||||
func OpenDBWithCluster(db ClusterQuerier) (*sql.DB, error) {
|
|
||||||
driver := NewClusterDriver(db)
|
|
||||||
connector, err := driver.OpenConnector("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sql.OpenDB(connector), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClusterDriver implements [driver.Driver] and driver.Connector for an existing [Querier]
|
|
||||||
type ClusterDriver struct {
|
|
||||||
db ClusterQuerier
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClusterDriver creates a new [driver.Driver] that uses an existing [ClusterQuerier]
|
|
||||||
func NewClusterDriver(db ClusterQuerier) *ClusterDriver {
|
|
||||||
return &ClusterDriver{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open implements [driver.Driver.Open]
|
|
||||||
func (d *ClusterDriver) Open(name string) (driver.Conn, error) {
|
|
||||||
return d.Connect(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenConnector implements [driver.DriverContext.OpenConnector]
|
|
||||||
func (d *ClusterDriver) OpenConnector(name string) (driver.Connector, error) {
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect implements [driver.Connector.Connect]
|
|
||||||
func (d *ClusterDriver) Connect(ctx context.Context) (driver.Conn, error) {
|
|
||||||
conn, err := d.db.Conn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &dbConn{conn: conn}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Driver implements [driver.Connector.Driver]
|
|
||||||
func (d *ClusterDriver) Driver() driver.Driver {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbConn implements driver.Conn with both context and legacy methods
|
|
||||||
type dbConn struct {
|
|
||||||
conn *sql.Conn
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare implements [driver.Conn.Prepare] (legacy method)
|
|
||||||
func (c *dbConn) Prepare(query string) (driver.Stmt, error) {
|
|
||||||
return c.PrepareContext(context.Background(), query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareContext implements [driver.ConnPrepareContext.PrepareContext]
|
|
||||||
func (c *dbConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
stmt, err := c.conn.PrepareContext(ctx, query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbStmt{stmt: stmt}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements [driver.Execer.Exec] (legacy method)
|
|
||||||
func (c *dbConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return c.ExecContext(context.Background(), query, namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecContext implements [driver.ExecerContext.ExecContext]
|
|
||||||
func (c *dbConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// Convert driver.NamedValue to any
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.ExecContext(ctx, query, interfaceArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query implements [driver.Queryer.Query] (legacy method)
|
|
||||||
func (c *dbConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return c.QueryContext(context.Background(), query, namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryContext implements [driver.QueryerContext.QueryContext]
|
|
||||||
func (c *dbConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// Convert driver.NamedValue to any
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := c.conn.QueryContext(ctx, query, interfaceArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbRows{rows: rows}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin implements [driver.Conn.Begin] (legacy method)
|
|
||||||
func (c *dbConn) Begin() (driver.Tx, error) {
|
|
||||||
return c.BeginTx(context.Background(), driver.TxOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeginTx implements [driver.ConnBeginTx.BeginTx]
|
|
||||||
func (c *dbConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
sqlOpts := &sql.TxOptions{
|
|
||||||
Isolation: sql.IsolationLevel(opts.Isolation),
|
|
||||||
ReadOnly: opts.ReadOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := c.conn.BeginTx(ctx, sqlOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbTx{tx: tx}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping implements [driver.Pinger.Ping]
|
|
||||||
func (c *dbConn) Ping(ctx context.Context) error {
|
|
||||||
return c.conn.PingContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Conn.Close]
|
|
||||||
func (c *dbConn) Close() error {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid implements [driver.Validator.IsValid]
|
|
||||||
func (c *dbConn) IsValid() bool {
|
|
||||||
// Ping with a short timeout to check if the connection is still valid
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return c.conn.PingContext(ctx) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbStmt implements [driver.Stmt] with both context and legacy methods
|
|
||||||
type dbStmt struct {
|
|
||||||
stmt *sql.Stmt
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Stmt.Close]
|
|
||||||
func (s *dbStmt) Close() error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.stmt.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Stmt.NumInput]
|
|
||||||
func (s *dbStmt) NumInput() int {
|
|
||||||
return -1 // Number of parameters is unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements [driver.Stmt.Exec] (legacy method)
|
|
||||||
func (s *dbStmt) Exec(args []driver.Value) (driver.Result, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return s.ExecContext(context.Background(), namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecContext implements [driver.StmtExecContext.ExecContext]
|
|
||||||
func (s *dbStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
return s.stmt.ExecContext(ctx, interfaceArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query implements [driver.Stmt.Query] (legacy method)
|
|
||||||
func (s *dbStmt) Query(args []driver.Value) (driver.Rows, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return s.QueryContext(context.Background(), namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryContext implements [driver.StmtQueryContext.QueryContext]
|
|
||||||
func (s *dbStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := s.stmt.QueryContext(ctx, interfaceArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbRows{rows: rows}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbRows implements [driver.Rows]
|
|
||||||
type dbRows struct {
|
|
||||||
rows *sql.Rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// Columns implements [driver.Rows.Columns]
|
|
||||||
func (r *dbRows) Columns() []string {
|
|
||||||
cols, err := r.rows.Columns()
|
|
||||||
if err != nil {
|
|
||||||
// This shouldn't happen if the query was successful
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return cols
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Rows.Close]
|
|
||||||
func (r *dbRows) Close() error {
|
|
||||||
return r.rows.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next implements [driver.Rows.Next]
|
|
||||||
func (r *dbRows) Next(dest []driver.Value) error {
|
|
||||||
if !r.rows.Next() {
|
|
||||||
if err := r.rows.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a slice of interfaces to scan into
|
|
||||||
scanArgs := make([]any, len(dest))
|
|
||||||
for i := range scanArgs {
|
|
||||||
scanArgs[i] = &dest[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.rows.Scan(scanArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbTx implements [driver.Tx]
|
|
||||||
type dbTx struct {
|
|
||||||
tx *sql.Tx
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit implements [driver.Tx.Commit]
|
|
||||||
func (t *dbTx) Commit() error {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
return t.tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rollback implements [driver.Tx.Rollback]
|
|
||||||
func (t *dbTx) Rollback() error {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
return t.tx.Rollback()
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDriver(t *testing.T) {
|
|
||||||
dbMaster, dbMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbMaster.Close()
|
|
||||||
dbMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(1, 0)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("master-dc1"))
|
|
||||||
|
|
||||||
dbDRMaster, dbDRMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbDRMaster.Close()
|
|
||||||
dbDRMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 40)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster1-dc2"))
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster"))
|
|
||||||
|
|
||||||
dbSlaveDC1, dbSlaveDC1Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC1.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
dbSlaveDC2, dbSlaveDC2Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC2.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
tctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
c, err := NewCluster[Querier](
|
|
||||||
WithClusterContext(tctx),
|
|
||||||
WithClusterNodeChecker(hasql.PostgreSQLChecker),
|
|
||||||
WithClusterNodePicker(NewCustomPicker[Querier](
|
|
||||||
CustomPickerMaxLag(100),
|
|
||||||
)),
|
|
||||||
WithClusterNodes(
|
|
||||||
ClusterNode{"slave-dc1", dbSlaveDC1, 1},
|
|
||||||
ClusterNode{"master-dc1", dbMaster, 1},
|
|
||||||
ClusterNode{"slave-dc2", dbSlaveDC2, 2},
|
|
||||||
ClusterNode{"drmaster1-dc2", dbDRMaster, 0},
|
|
||||||
),
|
|
||||||
WithClusterOptions(
|
|
||||||
hasql.WithUpdateInterval[Querier](2*time.Second),
|
|
||||||
hasql.WithUpdateTimeout[Querier](1*time.Second),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if err = c.WaitForNodes(tctx, hasql.Primary, hasql.Standby); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := OpenDBWithCluster(c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use context methods
|
|
||||||
row := db.QueryRowContext(NodeStateCriterion(t.Context(), hasql.Primary), "SELECT node_name as name")
|
|
||||||
if err = row.Err(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeName := ""
|
|
||||||
if err = row.Scan(&nodeName); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if nodeName != "master-dc1" {
|
|
||||||
t.Fatalf("invalid node_name %s != %s", "master-dc1", nodeName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrClusterChecker = errors.New("cluster node checker required")
|
|
||||||
ErrClusterDiscoverer = errors.New("cluster node discoverer required")
|
|
||||||
ErrClusterPicker = errors.New("cluster node picker required")
|
|
||||||
ErrorNoAliveNodes = errors.New("cluster no alive nodes")
|
|
||||||
)
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClusterOptions contains cluster specific options
|
|
||||||
type ClusterOptions struct {
|
|
||||||
NodeChecker hasql.NodeChecker
|
|
||||||
NodePicker hasql.NodePicker[Querier]
|
|
||||||
NodeDiscoverer hasql.NodeDiscoverer[Querier]
|
|
||||||
Options []hasql.ClusterOpt[Querier]
|
|
||||||
Context context.Context
|
|
||||||
Retries int
|
|
||||||
NodePriority map[string]int32
|
|
||||||
NodeStateCriterion hasql.NodeStateCriterion
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClusterOption apply cluster options to ClusterOptions
|
|
||||||
type ClusterOption func(*ClusterOptions)
|
|
||||||
|
|
||||||
// WithClusterNodeChecker pass hasql.NodeChecker to cluster options
|
|
||||||
func WithClusterNodeChecker(c hasql.NodeChecker) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodeChecker = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodePicker pass hasql.NodePicker to cluster options
|
|
||||||
func WithClusterNodePicker(p hasql.NodePicker[Querier]) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodePicker = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodeDiscoverer pass hasql.NodeDiscoverer to cluster options
|
|
||||||
func WithClusterNodeDiscoverer(d hasql.NodeDiscoverer[Querier]) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodeDiscoverer = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRetries retry count on other nodes in case of error
|
|
||||||
func WithRetries(n int) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.Retries = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterContext pass context.Context to cluster options and used for checks
|
|
||||||
func WithClusterContext(ctx context.Context) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.Context = ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterOptions pass hasql.ClusterOpt
|
|
||||||
func WithClusterOptions(opts ...hasql.ClusterOpt[Querier]) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.Options = append(o.Options, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodeStateCriterion pass default hasql.NodeStateCriterion
|
|
||||||
func WithClusterNodeStateCriterion(c hasql.NodeStateCriterion) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodeStateCriterion = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClusterNode struct {
|
|
||||||
Name string
|
|
||||||
DB Querier
|
|
||||||
Priority int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodes create cluster with static NodeDiscoverer
|
|
||||||
func WithClusterNodes(cns ...ClusterNode) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
nodes := make([]*hasql.Node[Querier], 0, len(cns))
|
|
||||||
if o.NodePriority == nil {
|
|
||||||
o.NodePriority = make(map[string]int32, len(cns))
|
|
||||||
}
|
|
||||||
for _, cn := range cns {
|
|
||||||
nodes = append(nodes, hasql.NewNode(cn.Name, cn.DB))
|
|
||||||
if cn.Priority == 0 {
|
|
||||||
cn.Priority = math.MaxInt32
|
|
||||||
}
|
|
||||||
o.NodePriority[cn.Name] = cn.Priority
|
|
||||||
}
|
|
||||||
o.NodeDiscoverer = hasql.NewStaticNodeDiscoverer(nodes...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type nodeStateCriterionKey struct{}
|
|
||||||
|
|
||||||
// NodeStateCriterion inject hasql.NodeStateCriterion to context
|
|
||||||
func NodeStateCriterion(ctx context.Context, c hasql.NodeStateCriterion) context.Context {
|
|
||||||
return context.WithValue(ctx, nodeStateCriterionKey{}, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) getNodeStateCriterion(ctx context.Context) hasql.NodeStateCriterion {
|
|
||||||
if v, ok := ctx.Value(nodeStateCriterionKey{}).(hasql.NodeStateCriterion); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return c.options.NodeStateCriterion
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// compile time guard
|
|
||||||
var _ hasql.NodePicker[Querier] = (*CustomPicker[Querier])(nil)
|
|
||||||
|
|
||||||
// CustomPickerOptions holds options to pick nodes
|
|
||||||
type CustomPickerOptions struct {
|
|
||||||
MaxLag int
|
|
||||||
Priority map[string]int32
|
|
||||||
Retries int
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomPickerOption func apply option to CustomPickerOptions
|
|
||||||
type CustomPickerOption func(*CustomPickerOptions)
|
|
||||||
|
|
||||||
// CustomPickerMaxLag specifies max lag for which node can be used
|
|
||||||
func CustomPickerMaxLag(n int) CustomPickerOption {
|
|
||||||
return func(o *CustomPickerOptions) {
|
|
||||||
o.MaxLag = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCustomPicker creates new node picker
|
|
||||||
func NewCustomPicker[T Querier](opts ...CustomPickerOption) *CustomPicker[Querier] {
|
|
||||||
options := CustomPickerOptions{}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return &CustomPicker[Querier]{opts: options}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomPicker holds node picker options
|
|
||||||
type CustomPicker[T Querier] struct {
|
|
||||||
opts CustomPickerOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// PickNode used to return specific node
|
|
||||||
func (p *CustomPicker[T]) PickNode(cnodes []hasql.CheckedNode[T]) hasql.CheckedNode[T] {
|
|
||||||
for _, n := range cnodes {
|
|
||||||
fmt.Printf("node %s\n", n.Node.String())
|
|
||||||
}
|
|
||||||
return cnodes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CustomPicker[T]) getPriority(nodeName string) int32 {
|
|
||||||
if prio, ok := p.opts.Priority[nodeName]; ok {
|
|
||||||
return prio
|
|
||||||
}
|
|
||||||
return math.MaxInt32 // Default to lowest priority
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareNodes used to sort nodes
|
|
||||||
func (p *CustomPicker[T]) CompareNodes(a, b hasql.CheckedNode[T]) int {
|
|
||||||
// Get replication lag values
|
|
||||||
aLag := a.Info.(interface{ ReplicationLag() int }).ReplicationLag()
|
|
||||||
bLag := b.Info.(interface{ ReplicationLag() int }).ReplicationLag()
|
|
||||||
|
|
||||||
// First check that lag lower then MaxLag
|
|
||||||
if aLag > p.opts.MaxLag && bLag > p.opts.MaxLag {
|
|
||||||
return 0 // both are equal
|
|
||||||
}
|
|
||||||
|
|
||||||
// If one node exceeds MaxLag and the other doesn't, prefer the one that doesn't
|
|
||||||
if aLag > p.opts.MaxLag {
|
|
||||||
return 1 // b is better
|
|
||||||
}
|
|
||||||
if bLag > p.opts.MaxLag {
|
|
||||||
return -1 // a is better
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get node priorities
|
|
||||||
aPrio := p.getPriority(a.Node.String())
|
|
||||||
bPrio := p.getPriority(b.Node.String())
|
|
||||||
|
|
||||||
// if both priority equals
|
|
||||||
if aPrio == bPrio {
|
|
||||||
// First compare by replication lag
|
|
||||||
if aLag < bLag {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if aLag > bLag {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// If replication lag is equal, compare by latency
|
|
||||||
aLatency := a.Info.(interface{ Latency() time.Duration }).Latency()
|
|
||||||
bLatency := b.Info.(interface{ Latency() time.Duration }).Latency()
|
|
||||||
|
|
||||||
if aLatency < bLatency {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if aLatency > bLatency {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// If lag and latency is equal
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// If priorities are different, prefer the node with lower priority value
|
|
||||||
if aPrio < bPrio {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module go.unistack.org/micro/v3
|
module go.unistack.org/micro/v3
|
||||||
|
|
||||||
go 1.24.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.1
|
dario.cat/mergo v1.0.1
|
||||||
|
|||||||
@@ -4,20 +4,18 @@ package logger
|
|||||||
type Level int8
|
type Level int8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TraceLevel usually used to find bugs, very verbose
|
// TraceLevel level usually used to find bugs, very verbose
|
||||||
TraceLevel Level = iota - 2
|
TraceLevel Level = iota - 2
|
||||||
// DebugLevel used only when enabled debugging
|
// DebugLevel level used only when enabled debugging
|
||||||
DebugLevel
|
DebugLevel
|
||||||
// InfoLevel used for general info about what's going on inside the application
|
// InfoLevel level used for general info about what's going on inside the application
|
||||||
InfoLevel
|
InfoLevel
|
||||||
// WarnLevel used for non-critical entries
|
// WarnLevel level used for non-critical entries
|
||||||
WarnLevel
|
WarnLevel
|
||||||
// ErrorLevel used for errors that should definitely be noted
|
// ErrorLevel level used for errors that should definitely be noted
|
||||||
ErrorLevel
|
ErrorLevel
|
||||||
// FatalLevel used for critical errors and then calls `os.Exit(1)`
|
// FatalLevel level used for critical errors and then calls `os.Exit(1)`
|
||||||
FatalLevel
|
FatalLevel
|
||||||
// NoneLevel used to disable logging
|
|
||||||
NoneLevel
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns logger level string representation
|
// String returns logger level string representation
|
||||||
@@ -35,8 +33,6 @@ func (l Level) String() string {
|
|||||||
return "error"
|
return "error"
|
||||||
case FatalLevel:
|
case FatalLevel:
|
||||||
return "fatal"
|
return "fatal"
|
||||||
case NoneLevel:
|
|
||||||
return "none"
|
|
||||||
}
|
}
|
||||||
return "info"
|
return "info"
|
||||||
}
|
}
|
||||||
@@ -62,8 +58,6 @@ func ParseLevel(lvl string) Level {
|
|||||||
return ErrorLevel
|
return ErrorLevel
|
||||||
case FatalLevel.String():
|
case FatalLevel.String():
|
||||||
return FatalLevel
|
return FatalLevel
|
||||||
case NoneLevel.String():
|
|
||||||
return NoneLevel
|
|
||||||
}
|
}
|
||||||
return InfoLevel
|
return InfoLevel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ type Options struct {
|
|||||||
AddStacktrace bool
|
AddStacktrace bool
|
||||||
// DedupKeys deduplicate keys in log output
|
// DedupKeys deduplicate keys in log output
|
||||||
DedupKeys bool
|
DedupKeys bool
|
||||||
// FatalFinalizers runs in order in [logger.Fatal] method
|
|
||||||
FatalFinalizers []func(context.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultFatalFinalizer = func(ctx context.Context) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions creates new options struct
|
// NewOptions creates new options struct
|
||||||
@@ -71,7 +65,6 @@ func NewOptions(opts ...Option) Options {
|
|||||||
AddSource: true,
|
AddSource: true,
|
||||||
TimeFunc: time.Now,
|
TimeFunc: time.Now,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
FatalFinalizers: []func(context.Context){DefaultFatalFinalizer},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WithMicroKeys()(&options)
|
WithMicroKeys()(&options)
|
||||||
@@ -83,13 +76,6 @@ func NewOptions(opts ...Option) Options {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFatalFinalizers set logger.Fatal finalizers
|
|
||||||
func WithFatalFinalizers(fncs ...func(context.Context)) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.FatalFinalizers = fncs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContextAttrFuncs appends default funcs for the context attrs filler
|
// WithContextAttrFuncs appends default funcs for the context attrs filler
|
||||||
func WithContextAttrFuncs(fncs ...ContextAttrFunc) Option {
|
func WithContextAttrFuncs(fncs ...ContextAttrFunc) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v3/semconv"
|
"go.unistack.org/micro/v3/semconv"
|
||||||
@@ -32,7 +34,6 @@ var (
|
|||||||
warnValue = slog.StringValue("warn")
|
warnValue = slog.StringValue("warn")
|
||||||
errorValue = slog.StringValue("error")
|
errorValue = slog.StringValue("error")
|
||||||
fatalValue = slog.StringValue("fatal")
|
fatalValue = slog.StringValue("fatal")
|
||||||
noneValue = slog.StringValue("none")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type wrapper struct {
|
type wrapper struct {
|
||||||
@@ -84,8 +85,6 @@ func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
|
|||||||
a.Value = errorValue
|
a.Value = errorValue
|
||||||
case lvl >= logger.FatalLevel:
|
case lvl >= logger.FatalLevel:
|
||||||
a.Value = fatalValue
|
a.Value = fatalValue
|
||||||
case lvl >= logger.NoneLevel:
|
|
||||||
a.Value = noneValue
|
|
||||||
default:
|
default:
|
||||||
a.Value = infoValue
|
a.Value = infoValue
|
||||||
}
|
}
|
||||||
@@ -229,12 +228,11 @@ func (s *slogLogger) Error(ctx context.Context, msg string, attrs ...interface{}
|
|||||||
|
|
||||||
func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) {
|
func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
s.printLog(ctx, logger.FatalLevel, msg, attrs...)
|
s.printLog(ctx, logger.FatalLevel, msg, attrs...)
|
||||||
for _, fn := range s.opts.FatalFinalizers {
|
|
||||||
fn(ctx)
|
|
||||||
}
|
|
||||||
if closer, ok := s.opts.Out.(io.Closer); ok {
|
if closer, ok := s.opts.Out.(io.Closer); ok {
|
||||||
closer.Close()
|
closer.Close()
|
||||||
}
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *slogLogger) Warn(ctx context.Context, msg string, attrs ...interface{}) {
|
func (s *slogLogger) Warn(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
@@ -318,8 +316,6 @@ func loggerToSlogLevel(level logger.Level) slog.Level {
|
|||||||
return slog.LevelDebug - 1
|
return slog.LevelDebug - 1
|
||||||
case logger.FatalLevel:
|
case logger.FatalLevel:
|
||||||
return slog.LevelError + 1
|
return slog.LevelError + 1
|
||||||
case logger.NoneLevel:
|
|
||||||
return slog.LevelError + 2
|
|
||||||
default:
|
default:
|
||||||
return slog.LevelInfo
|
return slog.LevelInfo
|
||||||
}
|
}
|
||||||
@@ -337,8 +333,6 @@ func slogToLoggerLevel(level slog.Level) logger.Level {
|
|||||||
return logger.TraceLevel
|
return logger.TraceLevel
|
||||||
case slog.LevelError + 1:
|
case slog.LevelError + 1:
|
||||||
return logger.FatalLevel
|
return logger.FatalLevel
|
||||||
case slog.LevelError + 2:
|
|
||||||
return logger.NoneLevel
|
|
||||||
default:
|
default:
|
||||||
return logger.InfoLevel
|
return logger.InfoLevel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,24 +36,6 @@ func TestStacktrace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoneLevel(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
l := NewLogger(logger.WithLevel(logger.NoneLevel), logger.WithOutput(buf),
|
|
||||||
WithHandlerFunc(slog.NewTextHandler),
|
|
||||||
logger.WithAddStacktrace(true),
|
|
||||||
)
|
|
||||||
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Error(ctx, "msg1", errors.New("err"))
|
|
||||||
|
|
||||||
if buf.Len() != 0 {
|
|
||||||
t.Fatalf("logger none level not works, buf contains: %s", buf.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDelayedBuffer(t *testing.T) {
|
func TestDelayedBuffer(t *testing.T) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
@@ -80,7 +62,7 @@ func TestTime(t *testing.T) {
|
|||||||
WithHandlerFunc(slog.NewTextHandler),
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
logger.WithAddStacktrace(true),
|
logger.WithAddStacktrace(true),
|
||||||
logger.WithTimeFunc(func() time.Time {
|
logger.WithTimeFunc(func() time.Time {
|
||||||
return time.Unix(0, 0).UTC()
|
return time.Unix(0, 0)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
||||||
@@ -89,7 +71,8 @@ func TestTime(t *testing.T) {
|
|||||||
|
|
||||||
l.Error(ctx, "msg1", errors.New("err"))
|
l.Error(ctx, "msg1", errors.New("err"))
|
||||||
|
|
||||||
if !bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T00:00:00.000000000Z`)) {
|
if !bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T03:00:00.000000000+03:00`)) &&
|
||||||
|
!bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T00:00:00.000000000Z`)) {
|
||||||
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,16 +406,15 @@ func TestLogger(t *testing.T) {
|
|||||||
func Test_WithContextAttrFunc(t *testing.T) {
|
func Test_WithContextAttrFunc(t *testing.T) {
|
||||||
loggerContextAttrFuncs := []logger.ContextAttrFunc{
|
loggerContextAttrFuncs := []logger.ContextAttrFunc{
|
||||||
func(ctx context.Context) []interface{} {
|
func(ctx context.Context) []interface{} {
|
||||||
md, ok := metadata.FromOutgoingContext(ctx)
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
attrs := make([]interface{}, 0, 10)
|
attrs := make([]interface{}, 0, 10)
|
||||||
for k, v := range md {
|
for k, v := range md {
|
||||||
key := strings.ToLower(k)
|
switch k {
|
||||||
switch key {
|
case "X-Request-Id", "Phone", "External-Id", "Source-Service", "X-App-Install-Id", "Client-Id", "Client-Ip":
|
||||||
case "x-request-id", "phone", "external-Id", "source-service", "x-app-install-id", "client-id", "client-ip":
|
attrs = append(attrs, strings.ToLower(k), v)
|
||||||
attrs = append(attrs, key, v)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return attrs
|
return attrs
|
||||||
@@ -442,7 +424,7 @@ func Test_WithContextAttrFunc(t *testing.T) {
|
|||||||
logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, loggerContextAttrFuncs...)
|
logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, loggerContextAttrFuncs...)
|
||||||
|
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
ctx = metadata.AppendOutgoingContext(ctx, "X-Request-Id", uuid.New().String(),
|
ctx = metadata.AppendIncomingContext(ctx, "X-Request-Id", uuid.New().String(),
|
||||||
"Source-Service", "Test-System")
|
"Source-Service", "Test-System")
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
@@ -455,39 +437,17 @@ func Test_WithContextAttrFunc(t *testing.T) {
|
|||||||
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"info"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test message"`))) {
|
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"info"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test message"`))) {
|
||||||
t.Fatalf("logger info, buf %s", buf.Bytes())
|
t.Fatalf("logger info, buf %s", buf.Bytes())
|
||||||
}
|
}
|
||||||
if !(bytes.Contains(buf.Bytes(), []byte(`"x-request-id":`))) {
|
if !(bytes.Contains(buf.Bytes(), []byte(`"x-request-id":"`))) {
|
||||||
t.Fatalf("logger info, buf %s", buf.Bytes())
|
t.Fatalf("logger info, buf %s", buf.Bytes())
|
||||||
}
|
}
|
||||||
if !(bytes.Contains(buf.Bytes(), []byte(`"source-service":"Test-System"`))) {
|
if !(bytes.Contains(buf.Bytes(), []byte(`"source-service":"Test-System"`))) {
|
||||||
t.Fatalf("logger info, buf %s", buf.Bytes())
|
t.Fatalf("logger info, buf %s", buf.Bytes())
|
||||||
}
|
}
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
omd, _ := metadata.FromOutgoingContext(ctx)
|
imd, _ := metadata.FromIncomingContext(ctx)
|
||||||
l.Info(ctx, "test message1")
|
l.Info(ctx, "test message1")
|
||||||
omd.Set("Source-Service", "Test-System2")
|
imd.Set("Source-Service", "Test-System2")
|
||||||
l.Info(ctx, "test message2")
|
l.Info(ctx, "test message2")
|
||||||
|
|
||||||
// t.Logf("xxx %s", buf.Bytes())
|
// t.Logf("xxx %s", buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFatalFinalizers(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
l := NewLogger(
|
|
||||||
logger.WithLevel(logger.TraceLevel),
|
|
||||||
logger.WithOutput(buf),
|
|
||||||
)
|
|
||||||
if err := l.Init(
|
|
||||||
logger.WithFatalFinalizers(func(ctx context.Context) {
|
|
||||||
l.Info(ctx, "fatal finalizer")
|
|
||||||
})); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
l.Fatal(ctx, "info_msg1")
|
|
||||||
if !bytes.Contains(buf.Bytes(), []byte("fatal finalizer")) {
|
|
||||||
t.Fatalf("logger dont have fatal message, buf %s", buf.Bytes())
|
|
||||||
}
|
|
||||||
if !bytes.Contains(buf.Bytes(), []byte("info_msg1")) {
|
|
||||||
t.Fatalf("logger dont have info_msg1 message, buf %s", buf.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sf = "0-+# "
|
const sf = "0-+# "
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnwrap(t *testing.T) {
|
func TestUnwrap(t *testing.T) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ package meter
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,11 +49,9 @@ type Meter interface {
|
|||||||
Set(opts ...Option) Meter
|
Set(opts ...Option) Meter
|
||||||
// Histogram get or create histogram
|
// Histogram get or create histogram
|
||||||
Histogram(name string, labels ...string) Histogram
|
Histogram(name string, labels ...string) Histogram
|
||||||
// HistogramExt get or create histogram with specified quantiles
|
|
||||||
HistogramExt(name string, quantiles []float64, labels ...string) Histogram
|
|
||||||
// Summary get or create summary
|
// Summary get or create summary
|
||||||
Summary(name string, labels ...string) Summary
|
Summary(name string, labels ...string) Summary
|
||||||
// SummaryExt get or create summary with specified quantiles and window time
|
// SummaryExt get or create summary with spcified quantiles and window time
|
||||||
SummaryExt(name string, window time.Duration, quantiles []float64, labels ...string) Summary
|
SummaryExt(name string, window time.Duration, quantiles []float64, labels ...string) Summary
|
||||||
// Write writes metrics to io.Writer
|
// Write writes metrics to io.Writer
|
||||||
Write(w io.Writer, opts ...Option) error
|
Write(w io.Writer, opts ...Option) error
|
||||||
@@ -61,8 +59,6 @@ type Meter interface {
|
|||||||
Options() Options
|
Options() Options
|
||||||
// String return meter type
|
// String return meter type
|
||||||
String() string
|
String() string
|
||||||
// Unregister metric name and drop all data
|
|
||||||
Unregister(name string, labels ...string) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counter is a counter
|
// Counter is a counter
|
||||||
@@ -84,11 +80,7 @@ type FloatCounter interface {
|
|||||||
|
|
||||||
// Gauge is a float64 gauge
|
// Gauge is a float64 gauge
|
||||||
type Gauge interface {
|
type Gauge interface {
|
||||||
Add(float64)
|
|
||||||
Get() float64
|
Get() float64
|
||||||
Set(float64)
|
|
||||||
Dec()
|
|
||||||
Inc()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Histogram is a histogram for non-negative values with automatically created buckets
|
// Histogram is a histogram for non-negative values with automatically created buckets
|
||||||
@@ -125,39 +117,6 @@ func BuildLabels(labels ...string) []string {
|
|||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
var spool = newStringsPool(500)
|
|
||||||
|
|
||||||
type stringsPool struct {
|
|
||||||
p *sync.Pool
|
|
||||||
c int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStringsPool(size int) *stringsPool {
|
|
||||||
p := &stringsPool{c: size}
|
|
||||||
p.p = &sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return &strings.Builder{}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *stringsPool) Cap() int {
|
|
||||||
return p.c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *stringsPool) Get() *strings.Builder {
|
|
||||||
return p.p.Get().(*strings.Builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *stringsPool) Put(b *strings.Builder) {
|
|
||||||
if b.Cap() > p.c {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.Reset()
|
|
||||||
p.p.Put(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildName used to combine metric with labels.
|
// BuildName used to combine metric with labels.
|
||||||
// If labels count is odd, drop last element
|
// If labels count is odd, drop last element
|
||||||
func BuildName(name string, labels ...string) string {
|
func BuildName(name string, labels ...string) string {
|
||||||
@@ -166,6 +125,8 @@ func BuildName(name string, labels ...string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(labels) > 2 {
|
if len(labels) > 2 {
|
||||||
|
sort.Sort(byKey(labels))
|
||||||
|
|
||||||
idx := 0
|
idx := 0
|
||||||
for {
|
for {
|
||||||
if labels[idx] == labels[idx+2] {
|
if labels[idx] == labels[idx+2] {
|
||||||
@@ -180,9 +141,7 @@ func BuildName(name string, labels ...string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b := spool.Get()
|
var b strings.Builder
|
||||||
defer spool.Put(b)
|
|
||||||
|
|
||||||
_, _ = b.WriteString(name)
|
_, _ = b.WriteString(name)
|
||||||
_, _ = b.WriteRune('{')
|
_, _ = b.WriteRune('{')
|
||||||
for idx := 0; idx < len(labels); idx += 2 {
|
for idx := 0; idx < len(labels); idx += 2 {
|
||||||
@@ -190,9 +149,8 @@ func BuildName(name string, labels ...string) string {
|
|||||||
_, _ = b.WriteRune(',')
|
_, _ = b.WriteRune(',')
|
||||||
}
|
}
|
||||||
_, _ = b.WriteString(labels[idx])
|
_, _ = b.WriteString(labels[idx])
|
||||||
_, _ = b.WriteString(`="`)
|
_, _ = b.WriteString(`=`)
|
||||||
_, _ = b.WriteString(labels[idx+1])
|
_, _ = b.WriteString(strconv.Quote(labels[idx+1]))
|
||||||
_, _ = b.WriteRune('"')
|
|
||||||
}
|
}
|
||||||
_, _ = b.WriteRune('}')
|
_, _ = b.WriteRune('}')
|
||||||
|
|
||||||
|
|||||||
@@ -50,12 +50,11 @@ func TestBuildName(t *testing.T) {
|
|||||||
data := map[string][]string{
|
data := map[string][]string{
|
||||||
`my_metric{firstlabel="value2",zerolabel="value3"}`: {
|
`my_metric{firstlabel="value2",zerolabel="value3"}`: {
|
||||||
"my_metric",
|
"my_metric",
|
||||||
"firstlabel", "value2",
|
"zerolabel", "value3", "firstlabel", "value2",
|
||||||
"zerolabel", "value3",
|
|
||||||
},
|
},
|
||||||
`my_metric{broker="broker2",register="mdns",server="tcp"}`: {
|
`my_metric{broker="broker2",register="mdns",server="tcp"}`: {
|
||||||
"my_metric",
|
"my_metric",
|
||||||
"broker", "broker1", "broker", "broker2", "register", "mdns", "server", "http", "server", "tcp",
|
"broker", "broker1", "broker", "broker2", "server", "http", "server", "tcp", "register", "mdns",
|
||||||
},
|
},
|
||||||
`my_metric{aaa="aaa"}`: {
|
`my_metric{aaa="aaa"}`: {
|
||||||
"my_metric",
|
"my_metric",
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ func (r *noopMeter) Name() string {
|
|||||||
return r.opts.Name
|
return r.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *noopMeter) Unregister(name string, labels ...string) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initialize options
|
// Init initialize options
|
||||||
func (r *noopMeter) Init(opts ...Option) error {
|
func (r *noopMeter) Init(opts ...Option) error {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -70,11 +66,6 @@ func (r *noopMeter) Histogram(_ string, labels ...string) Histogram {
|
|||||||
return &noopHistogram{labels: labels}
|
return &noopHistogram{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HistogramExt implements the Meter interface
|
|
||||||
func (r *noopMeter) HistogramExt(_ string, quantiles []float64, labels ...string) Histogram {
|
|
||||||
return &noopHistogram{labels: labels}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set implements the Meter interface
|
// Set implements the Meter interface
|
||||||
func (r *noopMeter) Set(opts ...Option) Meter {
|
func (r *noopMeter) Set(opts ...Option) Meter {
|
||||||
m := &noopMeter{opts: r.opts}
|
m := &noopMeter{opts: r.opts}
|
||||||
@@ -141,18 +132,6 @@ type noopGauge struct {
|
|||||||
labels []string
|
labels []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *noopGauge) Add(float64) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *noopGauge) Set(float64) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *noopGauge) Inc() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *noopGauge) Dec() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *noopGauge) Get() float64 {
|
func (r *noopGauge) Get() float64 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultQuantiles = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}
|
|
||||||
|
|
||||||
// Option powers the configuration for metrics implementations:
|
// Option powers the configuration for metrics implementations:
|
||||||
type Option func(*Options)
|
type Option func(*Options)
|
||||||
|
|
||||||
@@ -25,8 +23,6 @@ type Options struct {
|
|||||||
WriteProcessMetrics bool
|
WriteProcessMetrics bool
|
||||||
// WriteFDMetrics flag to write fd metrics
|
// WriteFDMetrics flag to write fd metrics
|
||||||
WriteFDMetrics bool
|
WriteFDMetrics bool
|
||||||
// Quantiles specifies buckets for histogram
|
|
||||||
Quantiles []float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions prepares a set of options:
|
// NewOptions prepares a set of options:
|
||||||
@@ -65,12 +61,14 @@ func Address(value string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quantiles defines the desired spread of statistics for histogram metrics:
|
/*
|
||||||
func Quantiles(quantiles []float64) Option {
|
// TimingObjectives defines the desired spread of statistics for histogram / timing metrics:
|
||||||
|
func TimingObjectives(value map[float64]float64) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Quantiles = quantiles
|
o.TimingObjectives = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Labels add the meter labels
|
// Labels add the meter labels
|
||||||
func Labels(ls ...string) Option {
|
func Labels(ls ...string) Option {
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ import (
|
|||||||
"go.unistack.org/micro/v3/util/rand"
|
"go.unistack.org/micro/v3/util/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultCodecs will be used to encode/decode
|
||||||
|
var DefaultCodecs = map[string]codec.Codec{
|
||||||
|
"application/octet-stream": codec.NewCodec(),
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultContentType = "application/json"
|
defaultContentType = "application/json"
|
||||||
)
|
)
|
||||||
@@ -88,6 +93,9 @@ func (n *noopServer) newCodec(contentType string) (codec.Codec, error) {
|
|||||||
if cf, ok := n.opts.Codecs[contentType]; ok {
|
if cf, ok := n.opts.Codecs[contentType]; ok {
|
||||||
return cf, nil
|
return cf, nil
|
||||||
}
|
}
|
||||||
|
if cf, ok := DefaultCodecs[contentType]; ok {
|
||||||
|
return cf, nil
|
||||||
|
}
|
||||||
return nil, codec.ErrUnknownContentType
|
return nil, codec.ErrUnknownContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user