Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

22
library/database/BUILD Normal file
View File

@@ -0,0 +1,22 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//library/database/bfs:all-srcs",
"//library/database/elastic:all-srcs",
"//library/database/hbase.v2:all-srcs",
"//library/database/orm:all-srcs",
"//library/database/sql:all-srcs",
"//library/database/tidb:all-srcs",
],
tags = ["automanaged"],
)

View File

@@ -0,0 +1,43 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["upload_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["upload.go"],
importpath = "go-common/library/database/bfs",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/time:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,8 @@
### bfs sdk
#### V1.0.1
> 1. 添加content type
#### V1.0.0
> 1. 新增图片上传方法
> 2. 新增水印图片生成方法

View File

@@ -0,0 +1,10 @@
# Owner
liangkai
zhapuyu
# Author
liangkai
zhuangzhewei
# Reviewer
zhapuyu

View File

@@ -0,0 +1,13 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- liangkai
- zhapuyu
- zhuangzhewei
labels:
- library
- library/database/bfs
reviewers:
- liangkai
- zhapuyu
- zhuangzhewei

View File

@@ -0,0 +1,218 @@
package bfs
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go-common/library/ecode"
xhttp "go-common/library/net/http/blademaster"
xtime "go-common/library/time"
)
const (
_appKey = "appkey"
_ts = "ts"
)
var (
_defaultHost = "http://api.bilibili.co"
_pathUpload = "/x/internal/upload"
_pathGenWatermark = "/x/internal/image/gen"
_defaultHTTPClientConfig = &xhttp.ClientConfig{
App: &xhttp.App{
Key: "3c4e41f926e51656",
Secret: "26a2095b60c24154521d24ae62b885bb",
},
Dial: xtime.Duration(1 * time.Second),
Timeout: xtime.Duration(1 * time.Second),
}
errBucket = errors.New("bucket is empty")
errFile = errors.New("file is empty")
)
// Config bfs upload config
type Config struct {
Host string
HTTPClient *xhttp.ClientConfig
}
// Request bfs upload request
type Request struct {
Bucket string
Dir string
ContentType string
Filename string
File []byte
WMKey string
WMText string
WMPaddingX uint32
WMPaddingY uint32
WMScale float64
}
// verify verify bfs request.
func (r *Request) verify() error {
if r.Bucket == "" {
return errBucket
}
if r.File == nil || len(r.File) == 0 {
return errFile
}
return nil
}
// BFS bfs instance
type BFS struct {
conf *Config
client *xhttp.Client
}
// New new a bfs client.
func New(c *Config) *BFS {
if c == nil {
c = &Config{
Host: _defaultHost,
HTTPClient: _defaultHTTPClientConfig,
}
}
return &BFS{
conf: c,
client: xhttp.NewClient(c.HTTPClient),
}
}
// Upload bfs internal upload.
func (b *BFS) Upload(ctx context.Context, req *Request) (location string, err error) {
var (
buf = &bytes.Buffer{}
bw io.Writer
request *http.Request
response *struct {
Code int `json:"code"`
Data struct {
ETag string `json:"etag"`
Location string `json:"location"`
} `json:"data"`
Message string `json:"message"`
}
url = b.conf.Host + _pathUpload + "?" + b.sign()
)
if err = req.verify(); err != nil {
return
}
w := multipart.NewWriter(buf)
if bw, err = w.CreateFormFile("file", "bfs-upload"); err != nil {
return
}
if _, err = bw.Write(req.File); err != nil {
return
}
w.WriteField("bucket", req.Bucket)
if req.Filename != "" {
w.WriteField("file_name", req.Filename)
}
if req.Dir != "" {
w.WriteField("dir", req.Dir)
}
if req.WMText != "" {
w.WriteField("wm_text", req.WMKey)
}
if req.WMKey != "" {
w.WriteField("wm_key", req.WMKey)
}
if req.WMPaddingX > 0 {
w.WriteField("wm_padding_x", fmt.Sprint(req.WMPaddingX))
}
if req.WMPaddingY > 0 {
w.WriteField("wm_padding_y", fmt.Sprint(req.WMPaddingY))
}
if req.WMScale > 0 {
w.WriteField("wm_scale", strconv.FormatFloat(req.WMScale, 'f', 2, 64))
}
if req.ContentType != "" {
w.WriteField("content_type", req.ContentType)
}
if err = w.Close(); err != nil {
return
}
if request, err = http.NewRequest(http.MethodPost, url, buf); err != nil {
return
}
request.Header.Set("Content-Type", w.FormDataContentType())
if err = b.client.Do(ctx, request, &response); err != nil {
return
}
if !ecode.Int(response.Code).Equal(ecode.OK) {
err = ecode.Int(response.Code)
return
}
location = response.Data.Location
return
}
// GenWatermark create watermark image by key and text.
func (b *BFS) GenWatermark(ctx context.Context, uploadKey, wmKey, wmText string, vertical bool, distance int) (location string, err error) {
var (
params = url.Values{}
uri = b.conf.Host + _pathGenWatermark
response *struct {
Code int `json:"code"`
Data struct {
Location string `json:"location"`
Width int `json:"width"`
Height int `json:"height"`
MD5 string `json:"md5"`
} `json:"data"`
Message string `json:"message"`
}
)
params.Set("upload_key", uploadKey)
params.Set("wm_key", wmKey)
params.Set("wm_text", wmText)
params.Set("wm_vertical", fmt.Sprint(vertical))
params.Set("distance", fmt.Sprint(distance))
if err = b.client.Post(ctx, uri, "", params, &response); err != nil {
return
}
if !ecode.Int(response.Code).Equal(ecode.OK) {
err = ecode.Int(response.Code)
return
}
location = response.Data.Location
return
}
// sign calc appkey and appsecret sign.
func (b *BFS) sign() string {
key := b.conf.HTTPClient.Key
secret := b.conf.HTTPClient.Secret
params := url.Values{}
params.Set(_appKey, key)
params.Set(_ts, strconv.FormatInt(time.Now().Unix(), 10))
tmp := params.Encode()
if strings.IndexByte(tmp, '+') > -1 {
tmp = strings.Replace(tmp, "+", "%20", -1)
}
var buf bytes.Buffer
buf.WriteString(tmp)
buf.WriteString(secret)
mh := md5.Sum(buf.Bytes())
// query
var qb bytes.Buffer
qb.WriteString(tmp)
qb.WriteString("&sign=")
qb.WriteString(hex.EncodeToString(mh[:]))
return qb.String()
}

View File

@@ -0,0 +1,49 @@
package bfs
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestUpload(t *testing.T) {
Convey("internal upload", t, func() {
b := New(nil)
req := &Request{
Bucket: "b",
ContentType: "application/json",
File: []byte("hello world"),
}
res, err := b.Upload(context.TODO(), req)
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
t.Logf("%+v", res)
})
}
func TestFilenameUpload(t *testing.T) {
Convey("internal upload by specify filename", t, func() {
b := New(nil)
req := &Request{
Bucket: "b",
Dir: "/test",
Filename: "test.plain",
ContentType: "application/json",
File: []byte("........"),
}
res, err := b.Upload(context.TODO(), req)
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
t.Logf("%+v", res)
})
}
func TestGenWatermark(t *testing.T) {
Convey("create watermark by key and text", t, func() {
b := New(nil)
location, err := b.GenWatermark(context.TODO(), "c605dd5324f91ea1", "comic", "hello world", true, 2)
So(err, ShouldBeNil)
t.Logf("location:%s", location)
})
}

View File

@@ -0,0 +1,53 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"query_test.go",
"update_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/queue/databus/report:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"query.go",
"update.go",
],
importpath = "go-common/library/database/elastic",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,11 @@
# Owner
haoguanwei
renwei
zhapuyu
# Author
wangjian
# Reviewer
zhapuyu
guanhuaxin

View File

@@ -0,0 +1,14 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- haoguanwei
- renwei
- wangjian
- zhapuyu
labels:
- library
- library/database/elastic
reviewers:
- guanhuaxin
- wangjian
- zhapuyu

View File

@@ -0,0 +1,578 @@
package elastic
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"go-common/library/ecode"
httpx "go-common/library/net/http/blademaster"
timex "go-common/library/time"
"github.com/pkg/errors"
)
const (
// OrderAsc order ascend
OrderAsc = "asc"
// OrderDesc order descend
OrderDesc = "desc"
// RangeScopeLoRo left open & right open
RangeScopeLoRo rangeScope = "( )"
// RangeScopeLoRc left open & right close
RangeScopeLoRc rangeScope = "( ]"
// RangeScopeLcRo left close & right open
RangeScopeLcRo rangeScope = "[ )"
// RangeScopeLcRc lect close & right close
RangeScopeLcRc rangeScope = "[ ]"
// NotTypeEq not type eq
NotTypeEq notType = "eq"
// NotTypeIn not type in
NotTypeIn notType = "in"
// NotTypeRange not type range
NotTypeRange notType = "range"
// LikeLevelHigh wildcard keyword
LikeLevelHigh likeLevel = "high"
// LikeLevelMiddle ngram(1,2)
LikeLevelMiddle likeLevel = "middle"
// LikeLevelLow match split word
LikeLevelLow likeLevel = "low"
// IndexTypeYear index by year
IndexTypeYear indexType = "year"
// IndexTypeMonth index by month
IndexTypeMonth indexType = "month"
// IndexTypeWeek index by week
IndexTypeWeek indexType = "week"
// IndexTypeDay index by day
IndexTypeDay indexType = "day"
// EnhancedModeGroupBy group by mode
EnhancedModeGroupBy enhancedType = "group_by"
// EnhancedModeDistinct distinct mode
EnhancedModeDistinct enhancedType = "distinct"
// EnhancedModeSum sum mode
EnhancedModeSum enhancedType = "sum"
)
type (
notType string
rangeScope string
likeLevel string
indexType string
enhancedType string
)
var (
_defaultHost = "http://manager.bilibili.co"
_pathQuery = "/x/admin/search/query"
_pathUpsert = "/x/admin/search/upsert"
_defaultHTTPClient = &httpx.ClientConfig{
App: &httpx.App{
Key: "3c4e41f926e51656",
Secret: "26a2095b60c24154521d24ae62b885bb",
},
Dial: timex.Duration(time.Second),
Timeout: timex.Duration(time.Second),
}
// for index by week
weeks = map[int]string{0: "0107", 1: "0815", 2: "1623", 3: "2431"}
)
// Config Elastic config
type Config struct {
Host string
HTTPClient *httpx.ClientConfig
}
// response query elastic response
type response struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
type query struct {
Fields []string `json:"fields"`
From string `json:"from"`
OrderScoreFirst bool `json:"order_score_first"`
OrderRandomSeed string `json:"order_random_seed"`
Highlight bool `json:"highlight"`
Pn int `json:"pn"`
Ps int `json:"ps"`
Order []map[string]string `json:"order,omitempty"`
Where where `json:"where,omitempty"`
}
func (q *query) string() (string, error) {
var (
sli []string
m = make(map[string]bool)
)
for _, i := range strings.Split(q.From, ",") {
if m[i] {
continue
}
m[i] = true
sli = append(sli, i)
}
q.From = strings.Join(sli, ",")
bs, err := json.Marshal(q)
if err != nil {
return "", err
}
return string(bs), nil
}
type where struct {
GroupBy string `json:"group_by,omitempty"`
Like []whereLike `json:"like,omitempty"`
Eq map[string]interface{} `json:"eq,omitempty"`
Or map[string]interface{} `json:"or,omitempty"`
In map[string][]interface{} `json:"in,omitempty"`
Range map[string]string `json:"range,omitempty"`
Combo []*Combo `json:"combo,omitempty"`
Not map[notType]map[string]bool `json:"not,omitempty"`
Enhanced []interface{} `json:"enhanced,omitempty"`
}
type whereLike struct {
Fields []string `json:"kw_fields"`
Words []string `json:"kw"`
Or bool `json:"or"`
Level likeLevel `json:"level"`
}
// Combo mix eq & in & range
type Combo struct {
EQ []map[string]interface{} `json:"eq,omitempty"`
In []map[string][]interface{} `json:"in,omitempty"`
Range []map[string]string `json:"range,omitempty"`
NotEQ []map[string]interface{} `json:"not_eq,omitempty"`
NotIn []map[string][]interface{} `json:"not_in,omitempty"`
NotRange []map[string]string `json:"not_range,omitempty"`
Min struct {
EQ int `json:"eq,omitempty"`
In int `json:"in,omitempty"`
Range int `json:"range,omitempty"`
NotEQ int `json:"not_eq,omitempty"`
NotIn int `json:"not_in,omitempty"`
NotRange int `json:"not_range,omitempty"`
Min int `json:"min"`
} `json:"min"`
}
// ComboEQ .
func (cmb *Combo) ComboEQ(eq []map[string]interface{}) *Combo {
cmb.EQ = append(cmb.EQ, eq...)
return cmb
}
// ComboRange .
func (cmb *Combo) ComboRange(r []map[string]string) *Combo {
cmb.Range = append(cmb.Range, r...)
return cmb
}
// ComboIn .
func (cmb *Combo) ComboIn(in []map[string][]interface{}) *Combo {
cmb.In = append(cmb.In, in...)
return cmb
}
// ComboNotEQ .
func (cmb *Combo) ComboNotEQ(eq []map[string]interface{}) *Combo {
cmb.NotEQ = append(cmb.NotEQ, eq...)
return cmb
}
// ComboNotRange .
func (cmb *Combo) ComboNotRange(r []map[string]string) *Combo {
cmb.NotRange = append(cmb.NotRange, r...)
return cmb
}
// ComboNotIn .
func (cmb *Combo) ComboNotIn(in []map[string][]interface{}) *Combo {
cmb.NotIn = append(cmb.NotIn, in...)
return cmb
}
// MinEQ .
func (cmb *Combo) MinEQ(min int) *Combo {
cmb.Min.EQ = min
return cmb
}
// MinIn .
func (cmb *Combo) MinIn(min int) *Combo {
cmb.Min.In = min
return cmb
}
// MinRange .
func (cmb *Combo) MinRange(min int) *Combo {
cmb.Min.Range = min
return cmb
}
// MinNotEQ .
func (cmb *Combo) MinNotEQ(min int) *Combo {
cmb.Min.NotEQ = min
return cmb
}
// MinNotIn .
func (cmb *Combo) MinNotIn(min int) *Combo {
cmb.Min.NotIn = min
return cmb
}
// MinNotRange .
func (cmb *Combo) MinNotRange(min int) *Combo {
cmb.Min.NotRange = min
return cmb
}
// MinAll .
func (cmb *Combo) MinAll(min int) *Combo {
cmb.Min.Min = min
return cmb
}
type groupBy struct {
Mode enhancedType `json:"mode"`
Field string `json:"field"`
Order []map[string]string `json:"order"`
}
type enhance struct {
Mode enhancedType `json:"mode"`
Field string `json:"field"`
Order []map[string]string `json:"order,omitempty"`
Size int `json:"size,omitempty"`
}
// Elastic clastic instance
type Elastic struct {
c *Config
client *httpx.Client
}
// NewElastic .
func NewElastic(c *Config) *Elastic {
if c == nil {
c = &Config{
Host: _defaultHost,
HTTPClient: _defaultHTTPClient,
}
}
return &Elastic{
c: c,
client: httpx.NewClient(c.HTTPClient),
}
}
// Request request to elastic
type Request struct {
*Elastic
q query
business string
}
// NewRequest new a request every search query
func (e *Elastic) NewRequest(business string) *Request {
return &Request{
Elastic: e,
business: business,
q: query{
Fields: []string{},
Highlight: false,
OrderScoreFirst: true,
OrderRandomSeed: "",
Pn: 1,
Ps: 10,
},
}
}
// Fields add query fields
func (r *Request) Fields(fields ...string) *Request {
r.q.Fields = append(r.q.Fields, fields...)
return r
}
// Index add query index
func (r *Request) Index(indexes ...string) *Request {
r.q.From = strings.Join(indexes, ",")
return r
}
// IndexByMod index by mod
func (r *Request) IndexByMod(prefix string, val, mod int64) *Request {
tmp := mod - 1
var digit int
for tmp > 0 {
tmp /= 10
digit++
}
format := fmt.Sprintf("%s_%%0%dd", prefix, digit)
r.q.From = fmt.Sprintf(format, val%mod)
return r
}
// IndexByTime index by time
func (r *Request) IndexByTime(prefix string, typ indexType, begin, end time.Time) *Request {
var (
buf bytes.Buffer
index string
indexes = make(map[string]struct{})
)
for {
year := begin.Format("2006")
month := begin.Format("01")
switch typ {
case IndexTypeYear:
index = strings.Join([]string{prefix, year}, "_")
case IndexTypeMonth:
index = strings.Join([]string{prefix, year, month}, "_")
case IndexTypeDay:
day := begin.Format("02")
index = strings.Join([]string{prefix, year, month, day}, "_")
case IndexTypeWeek:
index = strings.Join([]string{prefix, year, month, weeks[begin.Day()/8]}, "_")
}
if begin.After(end) && begin.Day() != end.Day() {
break
}
indexes[index] = struct{}{}
begin = begin.AddDate(0, 0, 1)
}
for i := range indexes {
buf.WriteString(i)
buf.WriteString(",")
}
r.q.From = strings.TrimSuffix(buf.String(), ",")
return r
}
// OrderScoreFirst switch for order score first
func (r *Request) OrderScoreFirst(v bool) *Request {
r.q.OrderScoreFirst = v
return r
}
// OrderRandomSeed switch for order random
func (r *Request) OrderRandomSeed(v string) *Request {
r.q.OrderRandomSeed = v
return r
}
// Highlight switch from highlight
func (r *Request) Highlight(v bool) *Request {
r.q.Highlight = v
return r
}
// Pn page number
func (r *Request) Pn(v int) *Request {
r.q.Pn = v
return r
}
// Ps page size
func (r *Request) Ps(v int) *Request {
r.q.Ps = v
return r
}
// Order filed sort
func (r *Request) Order(field, sort string) *Request {
if sort != OrderAsc {
sort = OrderDesc
}
r.q.Order = append(r.q.Order, map[string]string{field: sort})
return r
}
// WhereEq where qual
func (r *Request) WhereEq(field string, eq interface{}) *Request {
if r.q.Where.Eq == nil {
r.q.Where.Eq = make(map[string]interface{})
}
r.q.Where.Eq[field] = eq
return r
}
// WhereOr where or
func (r *Request) WhereOr(field string, or interface{}) *Request {
if r.q.Where.Or == nil {
r.q.Where.Or = make(map[string]interface{})
}
r.q.Where.Or[field] = or
return r
}
// WhereIn where in
func (r *Request) WhereIn(field string, in interface{}) *Request {
if r.q.Where.In == nil {
r.q.Where.In = make(map[string][]interface{})
}
switch v := in.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, string:
r.q.Where.In[field] = append(r.q.Where.In[field], v)
case []int:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []int64:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []string:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []int8:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []int16:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []int32:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []uint:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []uint8:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []uint16:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []uint32:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
case []uint64:
for _, i := range v {
r.q.Where.In[field] = append(r.q.Where.In[field], i)
}
}
return r
}
// WhereRange where range
func (r *Request) WhereRange(field string, start, end interface{}, scope rangeScope) *Request {
if r.q.Where.Range == nil {
r.q.Where.Range = make(map[string]string)
}
if start == nil {
start = ""
}
if end == nil {
end = ""
}
switch scope {
case RangeScopeLoRo:
r.q.Where.Range[field] = fmt.Sprintf("(%v,%v)", start, end)
case RangeScopeLoRc:
r.q.Where.Range[field] = fmt.Sprintf("(%v,%v]", start, end)
case RangeScopeLcRo:
r.q.Where.Range[field] = fmt.Sprintf("[%v,%v)", start, end)
case RangeScopeLcRc:
r.q.Where.Range[field] = fmt.Sprintf("[%v,%v]", start, end)
}
return r
}
// WhereNot where not
func (r *Request) WhereNot(typ notType, fields ...string) *Request {
if r.q.Where.Not == nil {
r.q.Where.Not = make(map[notType]map[string]bool)
}
if r.q.Where.Not[typ] == nil {
r.q.Where.Not[typ] = make(map[string]bool)
}
for _, v := range fields {
r.q.Where.Not[typ][v] = true
}
return r
}
// WhereLike where like
func (r *Request) WhereLike(fields, words []string, or bool, level likeLevel) *Request {
if len(fields) == 0 || len(words) == 0 {
return r
}
l := whereLike{Fields: fields, Words: words, Or: or, Level: level}
r.q.Where.Like = append(r.q.Where.Like, l)
return r
}
// WhereCombo where combo
func (r *Request) WhereCombo(cmb ...*Combo) *Request {
r.q.Where.Combo = append(r.q.Where.Combo, cmb...)
return r
}
// GroupBy where group by
func (r *Request) GroupBy(mode enhancedType, field string, order []map[string]string) *Request {
for _, i := range order {
for k, v := range i {
if v != OrderAsc {
i[k] = OrderDesc
}
}
}
r.q.Where.Enhanced = append(r.q.Where.Enhanced, groupBy{Mode: mode, Field: field, Order: order})
return r
}
// Sum where enhance sum
func (r *Request) Sum(field string) *Request {
r.q.Where.Enhanced = append(r.q.Where.Enhanced, enhance{Mode: EnhancedModeSum, Field: field})
return r
}
// Scan parse the query response data
func (r *Request) Scan(ctx context.Context, result interface{}) (err error) {
q, err := r.q.string()
if err != nil {
return
}
params := url.Values{}
params.Add("business", r.business)
params.Add("query", q)
response := new(response)
if err = r.client.Get(ctx, r.c.Host+_pathQuery, "", params, &response); err != nil {
return
}
if !ecode.Int(response.Code).Equal(ecode.OK) {
return ecode.Int(response.Code)
}
err = errors.Wrapf(json.Unmarshal(response.Data, &result), "scan(%s)", response.Data)
return
}
// Params get query parameters
func (r *Request) Params() string {
q, _ := r.q.string()
return fmt.Sprintf("business=%s&query=%s", r.business, q)
}

View File

@@ -0,0 +1,222 @@
package elastic
import (
"context"
"testing"
"time"
"go-common/library/queue/databus/report"
. "github.com/smartystreets/goconvey/convey"
)
func Test_UserActionLog(t *testing.T) {
Convey("elastic user action log", t, func() {
type page struct {
Num int `json:"num"`
Size int `json:"size"`
Total int `json:"total"`
}
var res struct {
Page *page `json:"page"`
Result []*report.UserActionLog `json:"result"`
}
r := NewElastic(nil).NewRequest("log_user_action").Index("log_user_action_21_2018_06_1623")
err := r.Scan(context.TODO(), &res)
So(err, ShouldBeNil)
t.Logf("query page(%+v)", res.Page)
t.Logf("query result(%v)", len(res.Result))
})
}
func Test_Scan(t *testing.T) {
Convey("scan", t, func() {
type page struct {
Num int `json:"num"`
Size int `json:"size"`
Total int `json:"total"`
}
type ticketData struct {
ID int `json:"id"`
Name string `json:"name"`
City int `json:"city"`
Province int `json:"province"`
}
var res struct {
Page *page `json:"page"`
Result []*ticketData `json:"result"`
}
r := NewElastic(nil).NewRequest("ticket_venue").Fields("id", "city").Index("ticket_venue").WhereEq("city", 310100).Order("ctime", OrderDesc).Order("id", OrderAsc).
WhereOr("province", 310000).WhereRange("id", 1, 2000000, RangeScopeLcRc).WhereLike([]string{"name"}, []string{"梅赛德斯奔驰文化中心"}, true, LikeLevelHigh)
err := r.Scan(context.TODO(), &res)
So(err, ShouldBeNil)
t.Logf("query page(%+v)", res.Page)
t.Logf("query result(%v)", len(res.Result))
})
}
func Test_Index(t *testing.T) {
Convey("example index by mod", t, func() {
oid := int64(8888888)
r := NewElastic(nil).NewRequest("LOG_USER_COIN")
r.IndexByMod("log_user_action", oid, 100)
q, _ := r.q.string()
So(q, ShouldContainSubstring, `"from":"log_user_action_88"`)
})
Convey("example index by time", t, func() {
Convey("indexByTime by week", func() {
// 按周划分索引的方式
r := NewElastic(nil).NewRequest("LOG_USER_COIN")
r.IndexByTime("log_user_action", IndexTypeWeek, time.Date(2018, 8, 25, 2, 0, 0, 0, time.Local), time.Date(2018, 9, 24, 2, 0, 0, 0, time.Local))
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_08_2431")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09_0107")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09_0815")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09_1623")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09_2431")
})
Convey("indexByTime by year", func() {
// 按年划分索引的方式
r := NewElastic(nil).NewRequest("LOG_USER_COIN")
r.IndexByTime("log_user_action", IndexTypeYear, time.Date(2017, 8, 25, 2, 0, 0, 0, time.Local), time.Date(2018, 9, 24, 2, 0, 0, 0, time.Local))
So(r.q.From, ShouldContainSubstring, "log_user_action_2017")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018")
})
Convey("indexByTime by month", func() {
// 按月划分索引的方式
r := NewElastic(nil).NewRequest("LOG_USER_COIN")
r.IndexByTime("log_user_action", IndexTypeMonth, time.Date(2018, 8, 25, 2, 0, 0, 0, time.Local), time.Date(2018, 9, 24, 2, 0, 0, 0, time.Local))
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_08")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09")
})
Convey("indexByTime by day", func() {
// 按天划分索引的方式
r := NewElastic(nil).NewRequest("LOG_USER_COIN")
r.IndexByTime("log_user_action", IndexTypeDay, time.Date(2018, 9, 23, 4, 0, 0, 0, time.Local), time.Date(2018, 9, 24, 2, 0, 0, 0, time.Local))
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09_23")
So(r.q.From, ShouldContainSubstring, "log_user_action_2018_09_24")
})
})
}
func Test_Combo_Simple(t *testing.T) {
Convey("combo", t, func() {
e := NewElastic(nil)
// 实现: (tid in (2,3,4,5,9,20,21,22)) && (tid in (35,38)) && (tid in (15,17,18))
cmbA := &Combo{}
cmbA.ComboIn([]map[string][]interface{}{
{"tid": {2, 3, 4, 5, 9, 20, 21, 22}},
}).MinIn(1).MinAll(1)
cmbB := &Combo{}
cmbB.ComboIn([]map[string][]interface{}{
{"tid": {35, 38}},
}).MinIn(1).MinAll(1)
cmbC := &Combo{}
cmbC.ComboIn([]map[string][]interface{}{
{"tid": {15, 17, 18}},
}).MinIn(1).MinAll(1)
r := e.NewRequest("").WhereCombo(cmbA, cmbB, cmbC)
q, _ := r.q.string()
So(q, ShouldContainSubstring, `"where":{"combo":[{"eq":null,"in":[{"tid":[2,3,4,5,9,20,21,22]}],"range":null,"min":{"eq":0,"in":1,"range":0,"min":1}},{"eq":null,"in":[{"tid":[35,38]}],"range":null,"min":{"eq":0,"in":1,"range":0,"min":1}},{"eq":null,"in":[{"tid":[15,17,18]}],"range":null,"min":{"eq":0,"in":1,"range":0,"min":1}}]}`)
})
}
func Test_Combo_Complicated(t *testing.T) {
Convey("combo not", t, func() {
e := NewElastic(nil)
cmb := &Combo{}
cmb.ComboIn([]map[string][]interface{}{
{"tid": {1, 2}},
{"tid_type": {2, 3}},
}).ComboRange([]map[string]string{
{"id": "(10,20)"},
}).ComboNotEQ([]map[string]interface{}{
{"aid": 122},
{"id": 677},
}).MinIn(1).MinRange(1).MinNotEQ(1).MinAll(1)
r := e.NewRequest("").WhereCombo(cmb)
q, _ := r.q.string()
So(q, ShouldContainSubstring, `"where":{"combo":[{"in":[{"tid":[1,2]},{"tid_type":[2,3]}],"range":[{"id":"(10,20)"}],"not_eq":[{"aid":122},{"id":677}],"min":{"in":1,"range":1,"not_eq":1,"min":1}}]}}`)
})
Convey("combo", t, func() {
e := NewElastic(nil)
// 实现:
// (aid=122 or id=677) && (tid in (1,2,3,21) or tid_type in (1,2,3)) && (id > 10) &&
// (aid=88 or fid=99) && (mid in (11,33) or id in (22,33)) && (2 < cid <= 10 || sid > 10)
cmbA := &Combo{}
cmbA.ComboEQ([]map[string]interface{}{
{"aid": 122},
{"id": 677},
}).ComboIn([]map[string][]interface{}{
{"tid": {1, 2, 3, 21}},
{"tid_type": {1, 2, 3}},
}).ComboRange([]map[string]string{
{"id": "(10,)"},
}).MinIn(1).MinEQ(1).MinRange(1).MinAll(1)
cmbB := &Combo{}
cmbB.ComboEQ([]map[string]interface{}{
{"aid": 88},
{"fid": 99},
}).ComboIn([]map[string][]interface{}{
{"mid": {11, 33}},
{"id": {22, 33}},
}).ComboRange([]map[string]string{
{"cid": "(2,4]"},
{"sid": "(10,)"},
}).MinEQ(1).MinIn(2).MinRange(1).MinAll(1)
r := e.NewRequest("").WhereCombo(cmbA, cmbB)
q, _ := r.q.string()
So(q, ShouldContainSubstring, `"where":{"combo":[{"eq":[{"aid":122},{"id":677}],"in":[{"tid":[1,2,3,21]},{"tid_type":[1,2,3]}],"range":[{"id":"(10,)"}],"min":{"eq":1,"in":1,"range":1,"min":1}},{"eq":[{"aid":88},{"fid":99}],"in":[{"mid":[11,33]},{"id":[22,33]}],"range":[{"cid":"(2,4]"},{"sid":"(10,)"}],"min":{"eq":1,"in":2,"range":1,"min":1}}]}`)
})
}
func Test_Query(t *testing.T) {
Convey("query conditions", t, func() {
e := NewElastic(nil)
r := e.NewRequest("").WhereEq("city", 310100)
q, _ := r.q.string()
So(q, ShouldContainSubstring, `"where":{"eq":{"city":310100}}`)
r = e.NewRequest("").WhereLike([]string{"a", "b"}, []string{"c", "d"}, true, LikeLevelHigh).WhereLike([]string{"e", "f"}, []string{"g", "h"}, false, LikeLevelMiddle)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"like":[{"kw_fields":["a","b"],"kw":["c","d"],"or":true,"level":"high"},{"kw_fields":["e","f"],"kw":["g","h"],"or":false,"level":"middle"}]}`)
r = e.NewRequest("").WhereNot(NotTypeEq, "province")
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"not":{"eq":{"province":true}}}`)
r = e.NewRequest("").WhereRange("id", 100, 500, RangeScopeLcRo).WhereRange("date", "2018-08-08 08:08:08", "2019-09-09 09:09:09", RangeScopeLoRo)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"range":{"date":"(2018-08-08 08:08:08,2019-09-09 09:09:09)","id":"[100,500)"}}`)
r = e.NewRequest("").WhereOr("city", 100000)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"or":{"city":100000}}`)
ids := []int64{100, 200, 300}
r = e.NewRequest("").WhereIn("city", ids)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"in":{"city":[100,200,300]}}`)
strs := []string{"a"}
r = e.NewRequest("").WhereIn("name", strs)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"in":{"name":["a"]}}`)
field := "a"
order := []map[string]string{{"a": "asc"}}
r = e.NewRequest("").GroupBy(EnhancedModeGroupBy, field, order)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"enhanced":[{"mode":"group_by","field":"a","order":[{"a":"asc"}]}]}`)
field = "a"
r = e.NewRequest("").Sum(field)
q, _ = r.q.string()
So(q, ShouldContainSubstring, `"where":{"enhanced":[{"mode":"sum","field":"a"}]}`)
})
}

View File

@@ -0,0 +1,108 @@
package elastic
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"go-common/library/ecode"
)
// Update elastic upsert
type Update struct {
*Elastic
business string
data map[string][]interface{}
insert bool
}
// NewUpdate new a request every update
func (e *Elastic) NewUpdate(business string) *Update {
return &Update{
Elastic: e,
business: business,
data: make(map[string][]interface{}),
}
}
// IndexByMod index by mod
func (us *Update) IndexByMod(prefix string, val, mod int64) string {
tmp := mod - 1
var digit int
for tmp > 0 {
tmp /= 10
digit++
}
format := fmt.Sprintf("%s_%%0%dd", prefix, digit)
return fmt.Sprintf(format, val%mod)
}
// IndexByTime index by time
func (us *Update) IndexByTime(prefix string, typ indexType, t time.Time) (index string) {
year := t.Format("2006")
month := t.Format("01")
switch typ {
case IndexTypeYear:
index = strings.Join([]string{prefix, year}, "_")
case IndexTypeMonth:
index = strings.Join([]string{prefix, year, month}, "_")
case IndexTypeDay:
day := t.Format("02")
index = strings.Join([]string{prefix, year, month, day}, "_")
case IndexTypeWeek:
index = strings.Join([]string{prefix, year, month, weeks[t.Day()/8]}, "_")
}
return
}
// AddData add data items to request 'data' param
func (us *Update) AddData(index string, data interface{}) *Update {
if data == nil {
return us
}
us.data[index] = append(us.data[index], data)
return us
}
// HasData weather data is empty or not
func (us *Update) HasData() bool {
if us.data == nil {
return false
}
return len(us.data) > 0
}
// Insert set insert flag, it means 'replace'
func (us *Update) Insert() *Update {
us.insert = true
return us
}
// Do post a request
func (us *Update) Do(ctx context.Context) (err error) {
data, err := json.Marshal(us.data)
if err != nil {
return
}
params := url.Values{}
params.Add("business", us.business)
params.Add("data", string(data))
params.Add("insert", fmt.Sprintf("%t", us.insert))
response := new(response)
if err = us.client.Post(ctx, us.c.Host+_pathUpsert, "", params, &response); err != nil {
return
}
if !ecode.Int(response.Code).Equal(ecode.OK) {
err = ecode.Int(response.Code)
}
return
}
// Params get query parameters
func (us *Update) Params() string {
data, _ := json.Marshal(us.data)
return fmt.Sprintf("business=%s&insert=%t&data=%s", us.business, us.insert, data)
}

View File

@@ -0,0 +1,119 @@
package elastic
import (
"context"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Upsert(t *testing.T) {
Convey("upsert", t, func() {
e := NewElastic(nil)
us := e.NewUpdate("articledata").Insert()
data := map[string]int{"id": 552, "share": 321}
us.AddData("articledata", data)
data = map[string]int{"id": 558, "share": 1137}
us.AddData("articledata", data)
t.Logf("params(%s)", us.Params())
err := us.Do(context.Background())
So(err, ShouldBeNil)
})
}
func Test_Upsert_ByMod(t *testing.T) {
Convey("upsert", t, func() {
data := make([]map[string]interface{}, 0)
d := map[string]interface{}{"id": 22, "share": 22, "oid": int64(2266)}
data = append(data, d)
d = map[string]interface{}{"id": 33, "share": 33, "oid": int64(3388)}
data = append(data, d)
e := NewElastic(nil)
us := e.NewUpdate("reply").Insert()
for _, v := range data {
oid, ok := v["oid"]
if !ok {
continue
}
oidReal, ok := oid.(int64) //must as same as interface type
if !ok {
continue
}
us.AddData(us.IndexByMod("reply_record", oidReal, 100), v)
}
t.Logf("params(%s)", us.Params())
if us.HasData() {
err := us.Do(context.Background())
So(err, ShouldBeNil)
} else {
So(us.HasData(), ShouldBeTrue)
}
})
}
func Test_Update_Index(t *testing.T) {
Convey("test update index", t, func() {
e := NewElastic(nil)
us := e.NewUpdate("reply").Insert()
Convey("example by mod 100", func() {
oid := int64(808)
index := us.IndexByMod("reply", oid, 100)
So(index, ShouldEqual, "reply_08")
})
Convey("example by mod 1000", func() {
oid := int64(808)
index := us.IndexByMod("reply", oid, 1000)
So(index, ShouldEqual, "reply_808")
})
Convey("example by mod 10000", func() {
oid := int64(808)
index := us.IndexByMod("reply", oid, 10000)
So(index, ShouldEqual, "reply_0808")
})
Convey("example by mod oid 0", func() {
oid := int64(0)
index := us.IndexByMod("reply", oid, 100)
So(index, ShouldEqual, "reply_00")
})
Convey("example by mod oid 20", func() {
oid := int64(808)
index := us.IndexByMod("reply", oid, 20)
So(index, ShouldEqual, "reply_08")
})
})
}
func Test_Upsert_ByTime(t *testing.T) {
Convey("upsert", t, func() {
data := make([]map[string]interface{}, 0)
d := map[string]interface{}{"id": 22, "share": 22, "ctime": time.Now().AddDate(-2, 0, 0)}
data = append(data, d)
d = map[string]interface{}{"id": 33, "share": 33, "ctime": time.Now().AddDate(-3, 0, 0)}
data = append(data, d)
e := NewElastic(nil)
us := e.NewUpdate("reply_list").Insert()
for _, v := range data {
ctime, ok := v["ctime"]
if !ok {
continue
}
ctimeReal, ok := ctime.(time.Time) //must as same as interface type
if !ok {
continue
}
indexName := us.IndexByTime("reply_list_hot", IndexTypeYear, ctimeReal)
v["ctime"] = ctimeReal.Format("2006-01-02 15:04:05")
us.AddData(indexName, v)
}
t.Logf("params(%s)", us.Params())
if us.HasData() {
err := us.Do(context.Background())
So(err, ShouldBeNil)
} else {
So(us.HasData(), ShouldBeTrue)
}
})
}

View File

@@ -0,0 +1,52 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["hbase_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/time:go_default_library",
"//vendor/github.com/tsuna/gohbase/hrpc:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"config.go",
"hbase.go",
],
importpath = "go-common/library/database/hbase.v2",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
"//library/stat:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/tsuna/gohbase:go_default_library",
"//vendor/github.com/tsuna/gohbase/hrpc:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,45 @@
### database/hbase
### 项目简介
Hbase Client进行封装加入了链路追踪和统计。
### usage
```go
package main
import (
"context"
"fmt"
"go-common/conf"
"go-common/database/hbase.v2"
)
func main() {
config := &hbase.Config{Zookeeper: &conf.Zookeeper{Addrs: []string{"localhost"}}}
client := hbase.NewClient(config)
values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}}
ctx := context.Background()
_, err := client.PutStr(ctx, "user", "user1", values)
if err != nil {
panic(err)
}
result, err := client.GetStr(ctx, "user", "user1")
if err != nil {
panic(err)
}
fmt.Printf("%v", result)
}
```
##### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
##### 依赖包
> 1.[gohbase](https://github.com/tsuna/gohbase)

View File

@@ -0,0 +1,23 @@
package hbase
import (
xtime "go-common/library/time"
)
// ZKConfig Server&Client settings.
type ZKConfig struct {
Root string
Addrs []string
Timeout xtime.Duration
}
// Config hbase config
type Config struct {
Zookeeper *ZKConfig
RPCQueueSize int
FlushInterval xtime.Duration
EffectiveUser string
RegionLookupTimeout xtime.Duration
RegionReadTimeout xtime.Duration
TestRowKey string
}

View File

@@ -0,0 +1,285 @@
package hbase
import (
"context"
"io"
"strings"
"time"
"github.com/tsuna/gohbase"
"github.com/tsuna/gohbase/hrpc"
"go-common/library/log"
"go-common/library/net/trace"
"go-common/library/stat"
)
var stats = stat.DB
const (
_family = "hbase_client_v2"
)
// Client hbase client.
type Client struct {
hc gohbase.Client
addr string
config *Config
}
func (c *Client) setTrace(ctx context.Context, call hrpc.Call, perr *error) func() {
now := time.Now()
if t, ok := trace.FromContext(ctx); ok {
t = t.Fork(_family, call.Name())
t.SetTag(trace.String(trace.TagAddress, c.addr), trace.String(trace.TagComment, string(call.Table())+"."+string(call.Key())))
return func() {
t.Finish(perr)
stats.Timing("hbase:"+call.Name(), int64(time.Since(now)/time.Millisecond))
}
}
return func() {
stats.Timing("hbase:"+call.Name(), int64(time.Since(now)/time.Millisecond))
}
}
// NewClient new a hbase client.
func NewClient(config *Config, options ...gohbase.Option) *Client {
zkquorum := strings.Join(config.Zookeeper.Addrs, ",")
if config.Zookeeper.Root != "" {
options = append(options, gohbase.ZookeeperRoot(config.Zookeeper.Root))
}
if config.Zookeeper.Timeout != 0 {
options = append(options, gohbase.ZookeeperTimeout(time.Duration(config.Zookeeper.Timeout)))
}
if config.RPCQueueSize != 0 {
log.Warn("RPCQueueSize configuration be ignored")
}
// force RpcQueueSize = 1, don't change it !!! it has reason (゜-゜)つロ
options = append(options, gohbase.RpcQueueSize(1))
if config.FlushInterval != 0 {
options = append(options, gohbase.FlushInterval(time.Duration(config.FlushInterval)))
}
if config.EffectiveUser != "" {
options = append(options, gohbase.EffectiveUser(config.EffectiveUser))
}
if config.RegionLookupTimeout != 0 {
options = append(options, gohbase.RegionLookupTimeout(time.Duration(config.RegionLookupTimeout)))
}
if config.RegionReadTimeout != 0 {
options = append(options, gohbase.RegionReadTimeout(time.Duration(config.RegionReadTimeout)))
}
hc := gohbase.NewClient(zkquorum, options...)
return &Client{
hc: hc,
addr: zkquorum,
config: config,
}
}
// ScanAll do scan command and return all result
// NOTE: if err != nil the results is safe for range operate even not result found
func (c *Client) ScanAll(ctx context.Context, table []byte, options ...func(hrpc.Call) error) (results []*hrpc.Result, err error) {
cursor, err := c.Scan(ctx, table, options...)
if err != nil {
return nil, err
}
for {
result, err := cursor.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
results = append(results, result)
}
return results, nil
}
type scanTrace struct {
hrpc.Scanner
err error
cancelTrace func()
}
func (s *scanTrace) Next() (*hrpc.Result, error) {
var result *hrpc.Result
result, s.err = s.Scanner.Next()
if s.err != nil {
if s.err == io.EOF {
// reset error for trace
s.err = nil
return result, io.EOF
}
s.cancelTrace()
return result, s.err
}
return result, s.err
}
func (s *scanTrace) Close() error {
defer s.cancelTrace()
s.err = s.Scanner.Close()
return s.err
}
// Scan do a scan command.
func (c *Client) Scan(ctx context.Context, table []byte, options ...func(hrpc.Call) error) (scanner hrpc.Scanner, err error) {
var scan *hrpc.Scan
scan, err = hrpc.NewScan(ctx, table, options...)
if err != nil {
return nil, err
}
scanner = c.hc.Scan(scan)
st := &scanTrace{
Scanner: scanner,
}
st.cancelTrace = c.setTrace(ctx, scan, &st.err)
return st, nil
}
// ScanStr scan string
func (c *Client) ScanStr(ctx context.Context, table string, options ...func(hrpc.Call) error) (hrpc.Scanner, error) {
return c.Scan(ctx, []byte(table), options...)
}
// ScanStrAll scan string
// NOTE: if err != nil the results is safe for range operate even not result found
func (c *Client) ScanStrAll(ctx context.Context, table string, options ...func(hrpc.Call) error) ([]*hrpc.Result, error) {
return c.ScanAll(ctx, []byte(table), options...)
}
// ScanRange get a scanner for the given table and key range.
// The range is half-open, i.e. [startRow; stopRow[ -- stopRow is not
// included in the range.
func (c *Client) ScanRange(ctx context.Context, table, startRow, stopRow []byte, options ...func(hrpc.Call) error) (scanner hrpc.Scanner, err error) {
var scan *hrpc.Scan
scan, err = hrpc.NewScanRange(ctx, table, startRow, stopRow, options...)
if err != nil {
return nil, err
}
scanner = c.hc.Scan(scan)
st := &scanTrace{
Scanner: scanner,
}
st.cancelTrace = c.setTrace(ctx, scan, &st.err)
return st, nil
}
// ScanRangeStr get a scanner for the given table and key range.
// The range is half-open, i.e. [startRow; stopRow[ -- stopRow is not
// included in the range.
func (c *Client) ScanRangeStr(ctx context.Context, table, startRow, stopRow string, options ...func(hrpc.Call) error) (hrpc.Scanner, error) {
return c.ScanRange(ctx, []byte(table), []byte(startRow), []byte(stopRow), options...)
}
// Get get result for the given table and row key.
// NOTE: if err != nil then result != nil, if result not exists result.Cells length is 0
func (c *Client) Get(ctx context.Context, table, key []byte, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) {
var get *hrpc.Get
get, err = hrpc.NewGet(ctx, table, key, options...)
if err != nil {
return nil, err
}
defer c.setTrace(ctx, get, &err)()
return c.hc.Get(get)
}
// GetStr do a get command.
// NOTE: if err != nil then result != nil, if result not exists result.Cells length is 0
func (c *Client) GetStr(ctx context.Context, table, key string, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) {
return c.Get(ctx, []byte(table), []byte(key), options...)
}
// PutStr insert the given family-column-values in the given row key of the given table.
func (c *Client) PutStr(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) {
var put *hrpc.Mutate
put, err = hrpc.NewPutStr(ctx, table, key, values, options...)
if err != nil {
return nil, err
}
defer c.setTrace(ctx, put, &err)()
return c.hc.Put(put)
}
// Delete is used to perform Delete operations on a single row.
// To delete entire row, values should be nil.
//
// To delete specific families, qualifiers map should be nil:
// map[string]map[string][]byte{
// "cf1": nil,
// "cf2": nil,
// }
//
// To delete specific qualifiers:
// map[string]map[string][]byte{
// "cf": map[string][]byte{
// "q1": nil,
// "q2": nil,
// },
// }
//
// To delete all versions before and at a timestamp, pass hrpc.Timestamp() option.
// By default all versions will be removed.
//
// To delete only a specific version at a timestamp, pass hrpc.DeleteOneVersion() option
// along with a timestamp. For delete specific qualifiers request, if timestamp is not
// passed, only the latest version will be removed. For delete specific families request,
// the timestamp should be passed or it will have no effect as it's an expensive
// operation to perform.
func (c *Client) Delete(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) {
var delete *hrpc.Mutate
delete, err = hrpc.NewDelStr(ctx, table, key, values, options...)
defer c.setTrace(ctx, delete, &err)()
return c.hc.Delete(delete)
}
// Append do a append command.
func (c *Client) Append(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) {
var append *hrpc.Mutate
append, err = hrpc.NewAppStr(ctx, table, key, values, options...)
defer c.setTrace(ctx, append, &err)()
return c.hc.Append(append)
}
// Increment the given values in HBase under the given table and key.
func (c *Client) Increment(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (result int64, err error) {
var increment *hrpc.Mutate
increment, err = hrpc.NewIncStr(ctx, table, key, values, options...)
if err != nil {
return 0, err
}
defer c.setTrace(ctx, increment, &err)()
return c.hc.Increment(increment)
}
// IncrementSingle increment the given value by amount in HBase under the given table, key, family and qualifier.
func (c *Client) IncrementSingle(ctx context.Context, table string, key string, family string, qualifier string, amount int64, options ...func(hrpc.Call) error) (result int64, err error) {
var increment *hrpc.Mutate
increment, err = hrpc.NewIncStrSingle(ctx, table, key, family, qualifier, amount, options...)
if err != nil {
return 0, err
}
defer c.setTrace(ctx, increment, &err)()
return c.hc.Increment(increment)
}
// Ping ping.
func (c *Client) Ping(ctx context.Context) (err error) {
testRowKey := "test"
if c.config.TestRowKey != "" {
testRowKey = c.config.TestRowKey
}
values := map[string]map[string][]byte{"test": map[string][]byte{"test": []byte("test")}}
_, err = c.PutStr(ctx, "test", testRowKey, values)
return
}
// Close close client.
func (c *Client) Close() error {
c.hc.Close()
return nil
}

View File

@@ -0,0 +1,164 @@
package hbase
import (
"context"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
"github.com/tsuna/gohbase/hrpc"
xtime "go-common/library/time"
)
var addrs []string
var client *Client
func TestMain(m *testing.M) {
addrsStr := os.Getenv("HBASE_TEST_ADDRS")
if addrsStr == "" {
println("HBASE_TEST_ADDRS not set skip test !!")
return
}
addrs = strings.Split(addrsStr, ",")
config := &Config{
Zookeeper: &ZKConfig{Root: "", Addrs: addrs, Timeout: xtime.Duration(time.Second)},
}
client = NewClient(config)
os.Exit(m.Run())
}
func TestPing(t *testing.T) {
if err := client.Ping(context.Background()); err != nil {
t.Errorf("ping meet err: %v", err)
}
}
func TestPutGetDelete(t *testing.T) {
ctx := context.Background()
values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}}
result, err := client.PutStr(ctx, "user", "user1", values)
if err != nil {
t.Fatal(err)
}
t.Logf("%v", result)
result, err = client.GetStr(ctx, "user", "user1")
if err != nil {
t.Fatal(err)
}
if len(result.Cells) != 2 {
t.Errorf("unexpect result, expect 2 cell, get %d", len(result.Cells))
}
_, err = client.Delete(ctx, "user", "user1", values)
if err != nil {
t.Fatal(err)
}
result, err = client.GetStr(ctx, "user", "user1")
if err != nil {
t.Fatal(err)
}
if len(result.Cells) > 0 {
t.Errorf("unexpect result, found cells")
}
}
func TestScan(t *testing.T) {
N := 10
ctx := context.Background()
values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}}
for i := 0; i < N; i++ {
_, err := client.PutStr(ctx, "user", fmt.Sprintf("scan_%d", i), values)
if err != nil {
t.Error(err)
}
}
results, err := client.ScanStrAll(ctx, "user")
if err != nil {
t.Fatal(err)
}
if len(results) != N {
t.Errorf("unexpect result expect %d result get %d", N, len(results))
}
iter, err := client.ScanStr(ctx, "user")
if err != nil {
t.Fatal(err)
}
defer iter.Close()
GN := 0
for {
_, err := iter.Next()
if err != nil {
if err == io.EOF {
break
}
t.Error(err)
}
GN++
}
if GN != N {
t.Errorf("unexpect result expect %d result get %d", N, GN)
}
for i := 0; i < N; i++ {
_, err := client.Delete(ctx, "user", fmt.Sprintf("scan_%d", i), nil)
if err != nil {
t.Errorf("delete error %s", err)
}
}
}
func TestScanRange(t *testing.T) {
N := 10
ctx := context.Background()
values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}}
for i := 0; i < N; i++ {
_, err := client.PutStr(ctx, "user", fmt.Sprintf("scan_%d", i), values)
if err != nil {
t.Error(err)
}
}
scanner, err := client.ScanRangeStr(ctx, "user", "scan_0", "scan_3")
if err != nil {
t.Fatal(err)
}
var results []*hrpc.Result
for {
result, err := scanner.Next()
if err != nil {
if err == io.EOF {
break
}
t.Fatal(err)
}
results = append(results, result)
}
if len(results) != 3 {
t.Errorf("unexpect result expect %d result get %d", N, len(results))
}
for i := 0; i < N; i++ {
_, err := client.Delete(ctx, "user", fmt.Sprintf("scan_%d", i), nil)
if err != nil {
t.Errorf("delete error %s", err)
}
}
}
func TestClose(t *testing.T) {
if err := client.Close(); err != nil {
t.Logf("Close meet error: %v", err)
}
if err := client.Ping(context.Background()); err == nil {
t.Errorf("ping return nil error after being closed")
}
}

View File

@@ -0,0 +1,36 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = [
"orm.go",
"timestamp.go",
],
importpath = "go-common/library/database/orm",
tags = ["automanaged"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/go-sql-driver/mysql:go_default_library",
"//vendor/github.com/jinzhu/gorm:go_default_library",
],
)

View File

@@ -0,0 +1,46 @@
package orm
import (
"strings"
"time"
"go-common/library/ecode"
"go-common/library/log"
xtime "go-common/library/time"
// database driver
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
// Config mysql config.
type Config struct {
DSN string // data source name.
Active int // pool
Idle int // pool
IdleTimeout xtime.Duration // connect max life time.
}
type ormLog struct{}
func (l ormLog) Print(v ...interface{}) {
log.Info(strings.Repeat("%v ", len(v)), v...)
}
func init() {
gorm.ErrRecordNotFound = ecode.NothingFound
}
// NewMySQL new db and retry connection when has error.
func NewMySQL(c *Config) (db *gorm.DB) {
db, err := gorm.Open("mysql", c.DSN)
if err != nil {
log.Error("db dsn(%s) error: %v", c.DSN, err)
panic(err)
}
db.DB().SetMaxIdleConns(c.Idle)
db.DB().SetMaxOpenConns(c.Active)
db.DB().SetConnMaxLifetime(time.Duration(c.IdleTimeout) / time.Second)
db.SetLogger(ormLog{})
return
}

View File

@@ -0,0 +1,35 @@
package orm
import (
. "github.com/jinzhu/gorm"
)
func init() {
DefaultCallback.Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
DefaultCallback.Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)
}
// updateTimeStampForCreateCallback will set `ctime`, `mtime` when creating
func updateTimeStampForCreateCallback(scope *Scope) {
if !scope.HasError() {
now := NowFunc()
if createdAtField, ok := scope.FieldByName("ctime"); ok {
if createdAtField.IsBlank {
createdAtField.Set(now)
}
}
if updatedAtField, ok := scope.FieldByName("mtime"); ok {
if updatedAtField.IsBlank {
updatedAtField.Set(now)
}
}
}
}
func updateTimeStampForUpdateCallback(scope *Scope) {
if _, ok := scope.Get("gorm:update_column"); !ok {
scope.SetColumn("mtime", NowFunc())
}
}

View File

@@ -0,0 +1,52 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = [
"mysql.go",
"sql.go",
],
importpath = "go-common/library/database/sql",
tags = ["automanaged"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/net/trace:go_default_library",
"//library/stat:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/go-sql-driver/mysql:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["mysql_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
],
)

View File

@@ -0,0 +1,18 @@
### database/sql
##### Version 1.2.1
> 1.支持上报熔断错误
##### Version 1.2.0
> 1.添加数据库读写分离
##### Version 1.1.1
> 1.修复部分函数中的context cancel leak
##### Version 1.1.0
> 1.添加context和timeout支持
> 2.添加breaker支持
> 3.更新driver支持context/pool timeout
##### Version 1.0.0
> 1.支持trace/stats

View File

@@ -0,0 +1,6 @@
# Author
maojian
# Reviewer
zhapuyu
chenzhihui

View File

@@ -0,0 +1,11 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- maojian
labels:
- library
- library/database/sql
reviewers:
- chenzhihui
- maojian
- zhapuyu

View File

@@ -0,0 +1,10 @@
#### database/sql
##### 项目简介
MySQL数据库驱动进行封装加入了链路追踪和统计。
##### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
##### 依赖包
> 1.[Go-MySQL-Driver](https://github.com/go-sql-driver/mysql)

View File

@@ -0,0 +1,40 @@
package sql
import (
"go-common/library/log"
"go-common/library/net/netutil/breaker"
"go-common/library/stat"
"go-common/library/time"
// database driver
_ "github.com/go-sql-driver/mysql"
)
var stats = stat.DB
// Config mysql config.
type Config struct {
Addr string // for trace
DSN string // write data source name.
ReadDSN []string // read data source name.
Active int // pool
Idle int // pool
IdleTimeout time.Duration // connect max life time.
QueryTimeout time.Duration // query sql timeout
ExecTimeout time.Duration // execute sql timeout
TranTimeout time.Duration // transaction sql timeout
Breaker *breaker.Config // breaker
}
// NewMySQL new db and retry connection when has error.
func NewMySQL(c *Config) (db *DB) {
if c.QueryTimeout == 0 || c.ExecTimeout == 0 || c.TranTimeout == 0 {
panic("mysql must be set query/execute/transction timeout")
}
db, err := Open(c)
if err != nil {
log.Error("open mysql error(%v)", err)
panic(err)
}
return
}

View File

@@ -0,0 +1,258 @@
package sql
import (
"context"
"database/sql"
"os"
"testing"
"time"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
)
func TestMySQL(t *testing.T) {
bc := &breaker.Config{
Window: xtime.Duration(10 * time.Second),
Sleep: xtime.Duration(10 * time.Second),
Bucket: 10,
Ratio: 0.5,
Request: 100,
}
dsn := os.Getenv("TEST_MYSQL_DSN")
if dsn == "" {
t.Skipf("TEST_MYSQL_DSN is empty, sql test skipped")
}
dsn = dsn + "?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8"
c := &Config{
Addr: "test",
DSN: dsn,
Active: 10,
Idle: 5,
IdleTimeout: xtime.Duration(time.Minute),
QueryTimeout: xtime.Duration(time.Minute),
ExecTimeout: xtime.Duration(time.Minute),
TranTimeout: xtime.Duration(time.Minute),
Breaker: bc,
}
db := NewMySQL(c)
defer db.Close()
testPing(t, db)
testTable(t, db)
testExec(t, db)
testQuery(t, db)
testQueryRow(t, db)
testPrepare(t, db)
testPrepared(t, db)
testTransaction(t, db)
testMaster(t, db)
testMasterPanic(t, db)
}
func testMaster(t *testing.T, db *DB) {
master := db.Master()
if len(master.read) != 0 {
t.Errorf("expect master read conn is 0, get %d", len(master.read))
}
if master.write != db.write {
t.Errorf("expect master write conn equal to origin db write conn")
}
}
func testMasterPanic(t *testing.T, db *DB) {
defer func() {
err := recover()
if err == nil || err != ErrNoMaster {
t.Errorf("expect panic err=ErrNoMaster get %v", err)
}
}()
db.Master().Master()
}
func testTransaction(t *testing.T, db *DB) {
var (
tx *Tx
err error
execSQL = "INSERT INTO test(name) VALUES(?)"
selSQL = "SELECT name FROM test WHERE name=?"
txstmt *Stmt
)
if tx, err = db.Begin(context.TODO()); err != nil {
t.Errorf("MySQL: db transaction Begin err(%v)", err)
tx.Rollback()
return
}
t.Log("MySQL: db transaction begin")
if txstmt, err = tx.Prepare(execSQL); err != nil {
t.Errorf("MySQL: tx.Prepare err(%v)", err)
}
if stmt := tx.Stmt(txstmt); stmt == nil {
t.Errorf("MySQL:tx.Stmt err(%v)", err)
}
// exec
if _, err = tx.Exec(execSQL, "tx1"); err != nil {
t.Errorf("MySQL: tx.Exec err(%v)", err)
tx.Rollback()
return
}
t.Logf("MySQL:tx.Exec tx1")
if _, err = tx.Exec(execSQL, "tx1"); err != nil {
t.Errorf("MySQL: tx.Exec err(%v)", err)
tx.Rollback()
return
}
t.Logf("MySQL:tx.Exec tx1")
// query
rows, err := tx.Query(selSQL, "tx2")
if err != nil {
t.Errorf("MySQL:tx.Query err(%v)", err)
tx.Rollback()
return
}
rows.Close()
t.Log("MySQL: tx.Query tx2")
// queryrow
var name string
row := tx.QueryRow(selSQL, "noexist")
if err = row.Scan(&name); err != sql.ErrNoRows {
t.Errorf("MySQL: queryRow name: noexist")
}
if err = tx.Commit(); err != nil {
t.Errorf("MySQL:tx.Commit err(%v)", err)
}
if err = tx.Commit(); err != nil {
t.Logf("MySQL:tx.Commit err(%v)", err)
}
if err = tx.Rollback(); err != nil {
t.Logf("MySQL:tx Rollback err(%v)", err)
}
}
func testPing(t *testing.T, db *DB) {
if err := db.Ping(context.TODO()); err != nil {
t.Errorf("MySQL: ping error(%v)", err)
t.FailNow()
} else {
t.Log("MySQL: ping ok")
}
}
func testTable(t *testing.T, db *DB) {
table := "CREATE TABLE IF NOT EXISTS `test` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID', `name` varchar(16) NOT NULL DEFAULT '' COMMENT '名称', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8"
if _, err := db.Exec(context.TODO(), table); err != nil {
t.Errorf("MySQL: create table error(%v)", err)
} else {
t.Log("MySQL: create table ok")
}
}
func testExec(t *testing.T, db *DB) {
sql := "INSERT INTO test(name) VALUES(?)"
if _, err := db.Exec(context.TODO(), sql, "test"); err != nil {
t.Errorf("MySQL: insert error(%v)", err)
} else {
t.Log("MySQL: insert ok")
}
}
func testQuery(t *testing.T, db *DB) {
sql := "SELECT name FROM test WHERE name=?"
rows, err := db.Query(context.TODO(), sql, "test")
if err != nil {
t.Errorf("MySQL: query error(%v)", err)
}
defer rows.Close()
for rows.Next() {
name := ""
if err := rows.Scan(&name); err != nil {
t.Errorf("MySQL: query scan error(%v)", err)
} else {
t.Logf("MySQL: query name: %s", name)
}
}
}
func testQueryRow(t *testing.T, db *DB) {
sql := "SELECT name FROM test WHERE name=?"
name := ""
row := db.QueryRow(context.TODO(), sql, "test")
if err := row.Scan(&name); err != nil {
t.Errorf("MySQL: queryRow error(%v)", err)
} else {
t.Logf("MySQL: queryRow name: %s", name)
}
}
func testPrepared(t *testing.T, db *DB) {
sql := "SELECT name FROM test WHERE name=?"
name := ""
stmt := db.Prepared(sql)
row := stmt.QueryRow(context.TODO(), "test")
if err := row.Scan(&name); err != nil {
t.Errorf("MySQL: prepared query error(%v)", err)
} else {
t.Logf("MySQL: prepared query name: %s", name)
}
if err := stmt.Close(); err != nil {
t.Errorf("MySQL:stmt.Close err(%v)", err)
}
}
func testPrepare(t *testing.T, db *DB) {
var (
selsql = "SELECT name FROM test WHERE name=?"
execsql = "INSERT INTO test(name) VALUES(?)"
name = ""
)
selstmt, err := db.Prepare(selsql)
if err != nil {
t.Errorf("MySQL:Prepare err(%v)", err)
return
}
row := selstmt.QueryRow(context.TODO(), "noexit")
if err = row.Scan(&name); err == sql.ErrNoRows {
t.Logf("MySQL: prepare query error(%v)", err)
} else {
t.Errorf("MySQL: prepared query name: noexist")
}
rows, err := selstmt.Query(context.TODO(), "test")
if err != nil {
t.Errorf("MySQL:stmt.Query err(%v)", err)
}
rows.Close()
execstmt, err := db.Prepare(execsql)
if err != nil {
t.Errorf("MySQL:Prepare err(%v)", err)
return
}
if _, err := execstmt.Exec(context.TODO(), "test"); err != nil {
t.Errorf("MySQL: stmt.Exec(%v)", err)
}
}
func BenchmarkMySQL(b *testing.B) {
c := &Config{
Addr: "test",
DSN: "test:test@tcp(172.16.0.148:3306)/test?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8",
Active: 10,
Idle: 5,
IdleTimeout: xtime.Duration(time.Minute),
}
db := NewMySQL(c)
defer db.Close()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
sql := "SELECT name FROM test WHERE name=?"
rows, err := db.Query(context.TODO(), sql, "test")
if err == nil {
for rows.Next() {
var name string
if err = rows.Scan(&name); err != nil {
break
}
}
rows.Close()
}
}
})
}

654
library/database/sql/sql.go Normal file
View File

@@ -0,0 +1,654 @@
package sql
import (
"context"
"database/sql"
"fmt"
"strings"
"sync/atomic"
"time"
"go-common/library/ecode"
"go-common/library/net/netutil/breaker"
"go-common/library/net/trace"
"github.com/pkg/errors"
)
const (
_family = "sql_client"
)
var (
// ErrStmtNil prepared stmt error
ErrStmtNil = errors.New("sql: prepare failed and stmt nil")
// ErrNoMaster is returned by Master when call master multiple times.
ErrNoMaster = errors.New("sql: no master instance")
// ErrNoRows is returned by Scan when QueryRow doesn't return a row.
// In such a case, QueryRow returns a placeholder *Row value that defers
// this error until a Scan.
ErrNoRows = sql.ErrNoRows
// ErrTxDone transaction done.
ErrTxDone = sql.ErrTxDone
)
// DB database.
type DB struct {
write *conn
read []*conn
idx int64
master *DB
}
// conn database connection
type conn struct {
*sql.DB
breaker breaker.Breaker
conf *Config
}
// Tx transaction.
type Tx struct {
db *conn
tx *sql.Tx
t trace.Trace
c context.Context
cancel func()
}
// Row row.
type Row struct {
err error
*sql.Row
db *conn
query string
args []interface{}
t trace.Trace
cancel func()
}
// Scan copies the columns from the matched row into the values pointed at by dest.
func (r *Row) Scan(dest ...interface{}) (err error) {
if r.t != nil {
defer r.t.Finish(&err)
}
if r.err != nil {
err = r.err
} else if r.Row == nil {
err = ErrStmtNil
}
if err != nil {
return
}
err = r.Row.Scan(dest...)
if r.cancel != nil {
r.cancel()
}
r.db.onBreaker(&err)
if err != ErrNoRows {
err = errors.Wrapf(err, "query %s args %+v", r.query, r.args)
}
return
}
// Rows rows.
type Rows struct {
*sql.Rows
cancel func()
}
// Close closes the Rows, preventing further enumeration. If Next is called
// and returns false and there are no further result sets,
// the Rows are closed automatically and it will suffice to check the
// result of Err. Close is idempotent and does not affect the result of Err.
func (rs *Rows) Close() (err error) {
err = errors.WithStack(rs.Rows.Close())
if rs.cancel != nil {
rs.cancel()
}
return
}
// Stmt prepared stmt.
type Stmt struct {
db *conn
tx bool
query string
stmt atomic.Value
t trace.Trace
}
// Open opens a database specified by its database driver name and a
// driver-specific data source name, usually consisting of at least a database
// name and connection information.
func Open(c *Config) (*DB, error) {
db := new(DB)
d, err := connect(c, c.DSN)
if err != nil {
return nil, err
}
brkGroup := breaker.NewGroup(c.Breaker)
brk := brkGroup.Get(c.Addr)
w := &conn{DB: d, breaker: brk, conf: c}
rs := make([]*conn, 0, len(c.ReadDSN))
for _, rd := range c.ReadDSN {
d, err := connect(c, rd)
if err != nil {
return nil, err
}
brk := brkGroup.Get(parseDSNAddr(rd))
r := &conn{DB: d, breaker: brk, conf: c}
rs = append(rs, r)
}
db.write = w
db.read = rs
db.master = &DB{write: db.write}
return db, nil
}
func connect(c *Config, dataSourceName string) (*sql.DB, error) {
d, err := sql.Open("mysql", dataSourceName)
if err != nil {
err = errors.WithStack(err)
return nil, err
}
d.SetMaxOpenConns(c.Active)
d.SetMaxIdleConns(c.Idle)
d.SetConnMaxLifetime(time.Duration(c.IdleTimeout))
return d, nil
}
// Begin starts a transaction. The isolation level is dependent on the driver.
func (db *DB) Begin(c context.Context) (tx *Tx, err error) {
return db.write.begin(c)
}
// Exec executes a query without returning any rows.
// The args are for any placeholder parameters in the query.
func (db *DB) Exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) {
return db.write.exec(c, query, args...)
}
// Prepare creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the returned
// statement. The caller must call the statement's Close method when the
// statement is no longer needed.
func (db *DB) Prepare(query string) (*Stmt, error) {
return db.write.prepare(query)
}
// Prepared creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the returned
// statement. The caller must call the statement's Close method when the
// statement is no longer needed.
func (db *DB) Prepared(query string) (stmt *Stmt) {
return db.write.prepared(query)
}
// Query executes a query that returns rows, typically a SELECT. The args are
// for any placeholder parameters in the query.
func (db *DB) Query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) {
idx := db.readIndex()
for i := range db.read {
if rows, err = db.read[(idx+i)%len(db.read)].query(c, query, args...); !ecode.ServiceUnavailable.Equal(err) {
return
}
}
return db.write.query(c, query, args...)
}
// QueryRow executes a query that is expected to return at most one row.
// QueryRow always returns a non-nil value. Errors are deferred until Row's
// Scan method is called.
func (db *DB) QueryRow(c context.Context, query string, args ...interface{}) *Row {
idx := db.readIndex()
for i := range db.read {
if row := db.read[(idx+i)%len(db.read)].queryRow(c, query, args...); !ecode.ServiceUnavailable.Equal(row.err) {
return row
}
}
return db.write.queryRow(c, query, args...)
}
func (db *DB) readIndex() int {
if len(db.read) == 0 {
return 0
}
v := atomic.AddInt64(&db.idx, 1)
return int(v) % len(db.read)
}
// Close closes the write and read database, releasing any open resources.
func (db *DB) Close() (err error) {
if e := db.write.Close(); e != nil {
err = errors.WithStack(e)
}
for _, rd := range db.read {
if e := rd.Close(); e != nil {
err = errors.WithStack(e)
}
}
return
}
// Ping verifies a connection to the database is still alive, establishing a
// connection if necessary.
func (db *DB) Ping(c context.Context) (err error) {
if err = db.write.ping(c); err != nil {
return
}
for _, rd := range db.read {
if err = rd.ping(c); err != nil {
return
}
}
return
}
// Master return *DB instance direct use master conn
// use this *DB instance only when you have some reason need to get result without any delay.
func (db *DB) Master() *DB {
if db.master == nil {
panic(ErrNoMaster)
}
return db.master
}
func (db *conn) onBreaker(err *error) {
if err != nil && *err != nil && *err != sql.ErrNoRows && *err != sql.ErrTxDone {
db.breaker.MarkFailed()
} else {
db.breaker.MarkSuccess()
}
}
func (db *conn) begin(c context.Context) (tx *Tx, err error) {
now := time.Now()
t, ok := trace.FromContext(c)
if ok {
t = t.Fork(_family, "begin")
t.SetTag(trace.String(trace.TagAddress, db.conf.Addr), trace.String(trace.TagComment, ""))
defer func() {
if err != nil {
t.Finish(&err)
}
}()
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("mysql:begin", "breaker")
return
}
_, c, cancel := db.conf.TranTimeout.Shrink(c)
rtx, err := db.BeginTx(c, nil)
stats.Timing("mysql:begin", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.WithStack(err)
cancel()
return
}
tx = &Tx{tx: rtx, t: t, db: db, c: c, cancel: cancel}
return
}
func (db *conn) exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) {
now := time.Now()
if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "exec")
t.SetTag(trace.String(trace.TagAddress, db.conf.Addr), trace.String(trace.TagComment, query))
defer t.Finish(&err)
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("mysql:exec", "breaker")
return
}
_, c, cancel := db.conf.ExecTimeout.Shrink(c)
res, err = db.ExecContext(c, query, args...)
cancel()
db.onBreaker(&err)
stats.Timing("mysql:exec", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "exec:%s, args:%+v", query, args)
}
return
}
func (db *conn) ping(c context.Context) (err error) {
now := time.Now()
if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "ping")
t.SetTag(trace.String(trace.TagAddress, db.conf.Addr), trace.String(trace.TagComment, ""))
defer t.Finish(&err)
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("mysql:ping", "breaker")
return
}
_, c, cancel := db.conf.ExecTimeout.Shrink(c)
err = db.PingContext(c)
cancel()
db.onBreaker(&err)
stats.Timing("mysql:ping", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.WithStack(err)
}
return
}
func (db *conn) prepare(query string) (*Stmt, error) {
stmt, err := db.Prepare(query)
if err != nil {
err = errors.Wrapf(err, "prepare %s", query)
return nil, err
}
st := &Stmt{query: query, db: db}
st.stmt.Store(stmt)
return st, nil
}
func (db *conn) prepared(query string) (stmt *Stmt) {
stmt = &Stmt{query: query, db: db}
s, err := db.Prepare(query)
if err == nil {
stmt.stmt.Store(s)
return
}
go func() {
for {
s, err := db.Prepare(query)
if err != nil {
time.Sleep(time.Second)
continue
}
stmt.stmt.Store(s)
return
}
}()
return
}
func (db *conn) query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) {
now := time.Now()
if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "query")
t.SetTag(trace.String(trace.TagAddress, db.conf.Addr), trace.String(trace.TagComment, query))
defer t.Finish(&err)
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("mysql:query", "breaker")
return
}
_, c, cancel := db.conf.QueryTimeout.Shrink(c)
rs, err := db.DB.QueryContext(c, query, args...)
db.onBreaker(&err)
stats.Timing("mysql:query", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "query:%s, args:%+v", query, args)
cancel()
return
}
rows = &Rows{Rows: rs, cancel: cancel}
return
}
func (db *conn) queryRow(c context.Context, query string, args ...interface{}) *Row {
now := time.Now()
t, ok := trace.FromContext(c)
if ok {
t = t.Fork(_family, "queryrow")
t.SetTag(trace.String(trace.TagAddress, db.conf.Addr), trace.String(trace.TagComment, query))
}
if err := db.breaker.Allow(); err != nil {
stats.Incr("mysql:queryrow", "breaker")
return &Row{db: db, t: t, err: err}
}
_, c, cancel := db.conf.QueryTimeout.Shrink(c)
r := db.DB.QueryRowContext(c, query, args...)
stats.Timing("mysql:queryrow", int64(time.Since(now)/time.Millisecond))
return &Row{db: db, Row: r, query: query, args: args, t: t, cancel: cancel}
}
// Close closes the statement.
func (s *Stmt) Close() (err error) {
if s == nil {
err = ErrStmtNil
return
}
stmt, ok := s.stmt.Load().(*sql.Stmt)
if ok {
err = errors.WithStack(stmt.Close())
}
return
}
// Exec executes a prepared statement with the given arguments and returns a
// Result summarizing the effect of the statement.
func (s *Stmt) Exec(c context.Context, args ...interface{}) (res sql.Result, err error) {
if s == nil {
err = ErrStmtNil
return
}
now := time.Now()
if s.tx {
if s.t != nil {
s.t.SetTag(trace.String(trace.TagAnnotation, s.query))
}
} else if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "exec")
t.SetTag(trace.String(trace.TagAddress, s.db.conf.Addr), trace.String(trace.TagComment, s.query))
defer t.Finish(&err)
}
if err = s.db.breaker.Allow(); err != nil {
stats.Incr("mysql:stmt:exec", "breaker")
return
}
stmt, ok := s.stmt.Load().(*sql.Stmt)
if !ok {
err = ErrStmtNil
return
}
_, c, cancel := s.db.conf.ExecTimeout.Shrink(c)
res, err = stmt.ExecContext(c, args...)
cancel()
s.db.onBreaker(&err)
stats.Timing("mysql:stmt:exec", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "exec:%s, args:%+v", s.query, args)
}
return
}
// Query executes a prepared query statement with the given arguments and
// returns the query results as a *Rows.
func (s *Stmt) Query(c context.Context, args ...interface{}) (rows *Rows, err error) {
if s == nil {
err = ErrStmtNil
return
}
if s.tx {
if s.t != nil {
s.t.SetTag(trace.String(trace.TagAnnotation, s.query))
}
} else if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "query")
t.SetTag(trace.String(trace.TagAddress, s.db.conf.Addr), trace.String(trace.TagComment, s.query))
defer t.Finish(&err)
}
if err = s.db.breaker.Allow(); err != nil {
stats.Incr("mysql:stmt:query", "breaker")
return
}
stmt, ok := s.stmt.Load().(*sql.Stmt)
if !ok {
err = ErrStmtNil
return
}
now := time.Now()
_, c, cancel := s.db.conf.QueryTimeout.Shrink(c)
rs, err := stmt.QueryContext(c, args...)
s.db.onBreaker(&err)
stats.Timing("mysql:stmt:query", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "query:%s, args:%+v", s.query, args)
cancel()
return
}
rows = &Rows{Rows: rs, cancel: cancel}
return
}
// QueryRow executes a prepared query statement with the given arguments.
// If an error occurs during the execution of the statement, that error will
// be returned by a call to Scan on the returned *Row, which is always non-nil.
// If the query selects no rows, the *Row's Scan will return ErrNoRows.
// Otherwise, the *Row's Scan scans the first selected row and discards the rest.
func (s *Stmt) QueryRow(c context.Context, args ...interface{}) (row *Row) {
now := time.Now()
row = &Row{db: s.db, query: s.query, args: args}
if s == nil {
row.err = ErrStmtNil
return
}
if s.tx {
if s.t != nil {
s.t.SetTag(trace.String(trace.TagAnnotation, s.query))
}
} else if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "queryrow")
t.SetTag(trace.String(trace.TagAddress, s.db.conf.Addr), trace.String(trace.TagComment, s.query))
row.t = t
}
if row.err = s.db.breaker.Allow(); row.err != nil {
stats.Incr("mysql:stmt:queryrow", "breaker")
return
}
stmt, ok := s.stmt.Load().(*sql.Stmt)
if !ok {
return
}
_, c, cancel := s.db.conf.QueryTimeout.Shrink(c)
row.Row = stmt.QueryRowContext(c, args...)
row.cancel = cancel
stats.Timing("mysql:stmt:queryrow", int64(time.Since(now)/time.Millisecond))
return
}
// Commit commits the transaction.
func (tx *Tx) Commit() (err error) {
err = tx.tx.Commit()
tx.cancel()
tx.db.onBreaker(&err)
if tx.t != nil {
tx.t.Finish(&err)
}
if err != nil {
err = errors.WithStack(err)
}
return
}
// Rollback aborts the transaction.
func (tx *Tx) Rollback() (err error) {
err = tx.tx.Rollback()
tx.cancel()
tx.db.onBreaker(&err)
if tx.t != nil {
tx.t.Finish(&err)
}
if err != nil {
err = errors.WithStack(err)
}
return
}
// Exec executes a query that doesn't return rows. For example: an INSERT and
// UPDATE.
func (tx *Tx) Exec(query string, args ...interface{}) (res sql.Result, err error) {
now := time.Now()
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("exec %s", query)))
}
res, err = tx.tx.ExecContext(tx.c, query, args...)
stats.Timing("mysql:tx:exec", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "exec:%s, args:%+v", query, args)
}
return
}
// Query executes a query that returns rows, typically a SELECT.
func (tx *Tx) Query(query string, args ...interface{}) (rows *Rows, err error) {
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("query %s", query)))
}
now := time.Now()
defer func() {
stats.Timing("mysql:tx:query", int64(time.Since(now)/time.Millisecond))
}()
rs, err := tx.tx.QueryContext(tx.c, query, args...)
if err == nil {
rows = &Rows{Rows: rs}
} else {
err = errors.Wrapf(err, "query:%s, args:%+v", query, args)
}
return
}
// QueryRow executes a query that is expected to return at most one row.
// QueryRow always returns a non-nil value. Errors are deferred until Row's
// Scan method is called.
func (tx *Tx) QueryRow(query string, args ...interface{}) *Row {
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("queryrow %s", query)))
}
now := time.Now()
defer func() {
stats.Timing("mysql:tx:queryrow", int64(time.Since(now)/time.Millisecond))
}()
r := tx.tx.QueryRowContext(tx.c, query, args...)
return &Row{Row: r, db: tx.db, query: query, args: args}
}
// Stmt returns a transaction-specific prepared statement from an existing statement.
func (tx *Tx) Stmt(stmt *Stmt) *Stmt {
as, ok := stmt.stmt.Load().(*sql.Stmt)
if !ok {
return nil
}
ts := tx.tx.StmtContext(tx.c, as)
st := &Stmt{query: stmt.query, tx: true, t: tx.t, db: tx.db}
st.stmt.Store(ts)
return st
}
// Prepare creates a prepared statement for use within a transaction.
// The returned statement operates within the transaction and can no longer be
// used once the transaction has been committed or rolled back.
// To use an existing prepared statement on this transaction, see Tx.Stmt.
func (tx *Tx) Prepare(query string) (*Stmt, error) {
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("prepare %s", query)))
}
stmt, err := tx.tx.Prepare(query)
if err != nil {
err = errors.Wrapf(err, "prepare %s", query)
return nil, err
}
st := &Stmt{query: query, tx: true, t: tx.t, db: tx.db}
st.stmt.Store(stmt)
return st, nil
}
// parseDSNAddr parse dsn name and return addr.
func parseDSNAddr(dsn string) (addr string) {
if dsn == "" {
return
}
part0 := strings.Split(dsn, "@")
if len(part0) > 1 {
part1 := strings.Split(part0[1], "?")
if len(part1) > 0 {
addr = part1[0]
}
}
return
}

View File

@@ -0,0 +1,59 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = [
"discovery.go",
"node_proc.go",
"sql.go",
"tidb.go",
],
importpath = "go-common/library/database/tidb",
tags = ["automanaged"],
deps = [
"//library/conf/env:go_default_library",
"//library/log:go_default_library",
"//library/naming:go_default_library",
"//library/naming/discovery:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/net/trace:go_default_library",
"//library/stat:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/go-sql-driver/mysql:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"sql_test.go",
"tidb_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
],
)

View File

@@ -0,0 +1,4 @@
### database/tidb
##### Version 1.0.0
> 1. 接入discovery

View File

@@ -0,0 +1,7 @@
# Author
wangxu01
# Reviewer
maojian
haoguanwei
zhapuyu

View File

@@ -0,0 +1,14 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- maojian
- wangxu01
labels:
- library
- library/database/tidb
reviewers:
- chenzhihui
- haoguanwei
- maojian
- wangxu01
- zhapuyu

View File

@@ -0,0 +1,17 @@
#### database/tidb
##### 项目简介
TiDB数据库驱动 对mysql驱动进行封装
##### 功能
1. 支持discovery服务发现 多节点直连
2. 支持通过lvs单一地址连接
3. 支持prepare绑定多个节点
4. 支持动态增减节点负载均衡
5. 日志区分运行节点
##### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
##### 依赖包
> 1.[Go-MySQL-Driver](https://github.com/go-sql-driver/mysql)

View File

@@ -0,0 +1,56 @@
package tidb
import (
"context"
"fmt"
"strings"
"time"
"go-common/library/conf/env"
"go-common/library/log"
"go-common/library/naming"
"go-common/library/naming/discovery"
)
var _schema = "tidb://"
func (db *DB) nodeList() (nodes []string) {
var (
insMap map[string][]*naming.Instance
ins []*naming.Instance
ok bool
)
if insMap, ok = db.dis.Fetch(context.Background()); !ok {
return
}
if ins, ok = insMap[env.Zone]; !ok || len(ins) == 0 {
return
}
for _, in := range ins {
for _, addr := range in.Addrs {
if strings.HasPrefix(addr, _schema) {
addr = strings.Replace(addr, _schema, "", -1)
nodes = append(nodes, addr)
}
}
}
log.Info("tidb get %s instances(%v)", db.appid, nodes)
return
}
func (db *DB) disc() (nodes []string) {
db.dis = discovery.Build(db.appid)
e := db.dis.Watch()
select {
case <-e:
nodes = db.nodeList()
case <-time.After(10 * time.Second):
panic("tidb init discovery err")
}
if len(nodes) == 0 {
panic(fmt.Sprintf("tidb %s no instance", db.appid))
}
go db.nodeproc(e)
log.Info("init tidb discvoery info successfully")
return
}

View File

@@ -0,0 +1,82 @@
package tidb
import (
"time"
"go-common/library/log"
)
func (db *DB) nodeproc(e <-chan struct{}) {
if db.dis == nil {
return
}
for {
<-e
nodes := db.nodeList()
if len(nodes) == 0 {
continue
}
cm := make(map[string]*conn)
var conns []*conn
for _, conn := range db.conns {
cm[conn.addr] = conn
}
for _, node := range nodes {
if cm[node] != nil {
conns = append(conns, cm[node])
continue
}
c, err := db.connectDSN(genDSN(db.conf.DSN, node))
if err == nil {
conns = append(conns, c)
} else {
log.Error("tidb: connect addr: %s err: %+v", node, err)
}
}
if len(conns) == 0 {
log.Error("tidb: no nodes ignore event")
continue
}
oldConns := db.conns
db.mutex.Lock()
db.conns = conns
db.mutex.Unlock()
log.Info("tidb: new nodes: %v", nodes)
var removedConn []*conn
for _, conn := range oldConns {
var exist bool
for _, c := range conns {
if c.addr == conn.addr {
exist = true
break
}
}
if !exist {
removedConn = append(removedConn, conn)
}
}
go db.closeConns(removedConn)
}
}
func (db *DB) closeConns(conns []*conn) {
if len(conns) == 0 {
return
}
du := db.conf.QueryTimeout
if db.conf.ExecTimeout > du {
du = db.conf.ExecTimeout
}
if db.conf.TranTimeout > du {
du = db.conf.TranTimeout
}
time.Sleep(time.Duration(du))
for _, conn := range conns {
err := conn.Close()
if err != nil {
log.Error("tidb: close removed conn: %s err: %v", conn.addr, err)
} else {
log.Info("tidb: close removed conn: %s", conn.addr)
}
}
}

View File

@@ -0,0 +1,715 @@
package tidb
import (
"context"
"database/sql"
"fmt"
"sync"
"sync/atomic"
"time"
"go-common/library/naming"
"go-common/library/net/netutil/breaker"
"go-common/library/net/trace"
"github.com/go-sql-driver/mysql"
"github.com/pkg/errors"
)
const (
_family = "tidb_client"
)
var (
// ErrStmtNil prepared stmt error
ErrStmtNil = errors.New("sql: prepare failed and stmt nil")
// ErrNoRows is returned by Scan when QueryRow doesn't return a row.
// In such a case, QueryRow returns a placeholder *Row value that defers
// this error until a Scan.
ErrNoRows = sql.ErrNoRows
// ErrTxDone transaction done.
ErrTxDone = sql.ErrTxDone
)
// DB database.
type DB struct {
conf *Config
conns []*conn
idx int64
dis naming.Resolver
appid string
mutex sync.RWMutex
breakerGroup *breaker.Group
}
// conn database connection
type conn struct {
*sql.DB
breaker breaker.Breaker
conf *Config
addr string
}
// Tx transaction.
type Tx struct {
db *conn
tx *sql.Tx
t trace.Trace
c context.Context
cancel func()
}
// Row row.
type Row struct {
err error
*sql.Row
db *conn
query string
args []interface{}
t trace.Trace
cancel func()
}
// Scan copies the columns from the matched row into the values pointed at by dest.
func (r *Row) Scan(dest ...interface{}) (err error) {
if r.t != nil {
defer r.t.Finish(&err)
}
if r.err != nil {
err = r.err
} else if r.Row == nil {
err = ErrStmtNil
}
if err != nil {
return
}
err = r.Row.Scan(dest...)
if r.cancel != nil {
r.cancel()
}
r.db.onBreaker(&err)
if err != ErrNoRows {
err = errors.Wrapf(err, "addr: %s, query %s args %+v", r.db.addr, r.query, r.args)
}
return
}
// Rows rows.
type Rows struct {
*sql.Rows
cancel func()
}
// Close closes the Rows, preventing further enumeration. If Next is called
// and returns false and there are no further result sets,
// the Rows are closed automatically and it will suffice to check the
// result of Err. Close is idempotent and does not affect the result of Err.
func (rs *Rows) Close() (err error) {
err = errors.WithStack(rs.Rows.Close())
if rs.cancel != nil {
rs.cancel()
}
return
}
// Stmt prepared stmt.
type Stmt struct {
db *conn
tx bool
query string
stmt atomic.Value
t trace.Trace
}
// Stmts random prepared stmt.
type Stmts struct {
query string
sts map[string]*Stmt
mu sync.RWMutex
db *DB
}
// Open opens a database specified by its database driver name and a
// driver-specific data source name, usually consisting of at least a database
// name and connection information.
func Open(c *Config) (db *DB, err error) {
db = &DB{conf: c, breakerGroup: breaker.NewGroup(c.Breaker)}
cfg, err := mysql.ParseDSN(c.DSN)
if err != nil {
return
}
var dsns []string
if cfg.Net == "discovery" {
db.appid = cfg.Addr
for _, addr := range db.disc() {
dsns = append(dsns, genDSN(c.DSN, addr))
}
} else {
dsns = append(dsns, c.DSN)
}
cs := make([]*conn, 0, len(dsns))
for _, dsn := range dsns {
r, err := db.connectDSN(dsn)
if err != nil {
return db, err
}
cs = append(cs, r)
}
db.conns = cs
return
}
func (db *DB) connectDSN(dsn string) (c *conn, err error) {
d, err := connect(db.conf, dsn)
if err != nil {
return
}
addr := parseDSNAddr(dsn)
brk := db.breakerGroup.Get(addr)
c = &conn{DB: d, breaker: brk, conf: db.conf, addr: addr}
return
}
func connect(c *Config, dataSourceName string) (*sql.DB, error) {
d, err := sql.Open("mysql", dataSourceName)
if err != nil {
err = errors.WithStack(err)
return nil, err
}
d.SetMaxOpenConns(c.Active)
d.SetMaxIdleConns(c.Idle)
d.SetConnMaxLifetime(time.Duration(c.IdleTimeout))
return d, nil
}
func (db *DB) conn() (c *conn) {
db.mutex.RLock()
c = db.conns[db.index()]
db.mutex.RUnlock()
return
}
// Begin starts a transaction. The isolation level is dependent on the driver.
func (db *DB) Begin(c context.Context) (tx *Tx, err error) {
return db.conn().begin(c)
}
// Exec executes a query without returning any rows.
// The args are for any placeholder parameters in the query.
func (db *DB) Exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) {
return db.conn().exec(c, query, args...)
}
// Prepare creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the returned
// statement. The caller must call the statement's Close method when the
// statement is no longer needed.
func (db *DB) Prepare(query string) (*Stmt, error) {
return db.conn().prepare(query)
}
// Prepared creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the returned
// statement. The caller must call the statement's Close method when the
// statement is no longer needed.
func (db *DB) Prepared(query string) (s *Stmts) {
s = &Stmts{query: query, sts: make(map[string]*Stmt), db: db}
for _, c := range db.conns {
st := c.prepared(query)
s.mu.Lock()
s.sts[c.addr] = st
s.mu.Unlock()
}
return
}
// Query executes a query that returns rows, typically a SELECT. The args are
// for any placeholder parameters in the query.
func (db *DB) Query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) {
return db.conn().query(c, query, args...)
}
// QueryRow executes a query that is expected to return at most one row.
// QueryRow always returns a non-nil value. Errors are deferred until Row's
// Scan method is called.
func (db *DB) QueryRow(c context.Context, query string, args ...interface{}) *Row {
return db.conn().queryRow(c, query, args...)
}
func (db *DB) index() int {
if len(db.conns) == 1 {
return 0
}
v := atomic.AddInt64(&db.idx, 1)
return int(v) % len(db.conns)
}
// Close closes the databases, releasing any open resources.
func (db *DB) Close() (err error) {
db.mutex.RLock()
defer db.mutex.RUnlock()
for _, d := range db.conns {
if e := d.Close(); e != nil {
err = errors.WithStack(e)
}
}
return
}
// Ping verifies a connection to the database is still alive, establishing a
// connection if necessary.
func (db *DB) Ping(c context.Context) (err error) {
if err = db.conn().ping(c); err != nil {
return
}
return
}
func (db *conn) onBreaker(err *error) {
if err != nil && *err != nil && *err != sql.ErrNoRows && *err != sql.ErrTxDone {
db.breaker.MarkFailed()
} else {
db.breaker.MarkSuccess()
}
}
func (db *conn) begin(c context.Context) (tx *Tx, err error) {
now := time.Now()
t, ok := trace.FromContext(c)
if ok {
t = t.Fork(_family, "begin")
t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, ""))
defer func() {
if err != nil {
t.Finish(&err)
}
}()
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("tidb:begin", "breaker")
return
}
_, c, cancel := db.conf.TranTimeout.Shrink(c)
rtx, err := db.BeginTx(c, nil)
stats.Timing("tidb:begin", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.WithStack(err)
cancel()
return
}
tx = &Tx{tx: rtx, t: t, db: db, c: c, cancel: cancel}
return
}
func (db *conn) exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) {
now := time.Now()
if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "exec")
t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query))
defer t.Finish(&err)
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("tidb:exec", "breaker")
return
}
_, c, cancel := db.conf.ExecTimeout.Shrink(c)
res, err = db.ExecContext(c, query, args...)
cancel()
db.onBreaker(&err)
stats.Timing("tidb:exec", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "addr: %s exec:%s, args:%+v", db.addr, query, args)
}
return
}
func (db *conn) ping(c context.Context) (err error) {
now := time.Now()
if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "ping")
t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, ""))
defer t.Finish(&err)
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("tidb:ping", "breaker")
return
}
_, c, cancel := db.conf.ExecTimeout.Shrink(c)
err = db.PingContext(c)
cancel()
db.onBreaker(&err)
stats.Timing("tidb:ping", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.WithStack(err)
}
return
}
func (db *conn) prepare(query string) (*Stmt, error) {
stmt, err := db.Prepare(query)
if err != nil {
err = errors.Wrapf(err, "addr: %s prepare %s", db.addr, query)
return nil, err
}
st := &Stmt{query: query, db: db}
st.stmt.Store(stmt)
return st, nil
}
func (db *conn) prepared(query string) (stmt *Stmt) {
stmt = &Stmt{query: query, db: db}
s, err := db.Prepare(query)
if err == nil {
stmt.stmt.Store(s)
return
}
return
}
func (db *conn) query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) {
now := time.Now()
if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "query")
t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query))
defer t.Finish(&err)
}
if err = db.breaker.Allow(); err != nil {
stats.Incr("tidb:query", "breaker")
return
}
_, c, cancel := db.conf.QueryTimeout.Shrink(c)
rs, err := db.DB.QueryContext(c, query, args...)
db.onBreaker(&err)
stats.Timing("tidb:query", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "addr: %s, query:%s, args:%+v", db.addr, query, args)
cancel()
return
}
rows = &Rows{Rows: rs, cancel: cancel}
return
}
func (db *conn) queryRow(c context.Context, query string, args ...interface{}) *Row {
now := time.Now()
t, ok := trace.FromContext(c)
if ok {
t = t.Fork(_family, "queryrow")
t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query))
}
if err := db.breaker.Allow(); err != nil {
stats.Incr("tidb:queryrow", "breaker")
return &Row{db: db, t: t, err: err}
}
_, c, cancel := db.conf.QueryTimeout.Shrink(c)
r := db.DB.QueryRowContext(c, query, args...)
stats.Timing("tidb:queryrow", int64(time.Since(now)/time.Millisecond))
return &Row{db: db, Row: r, query: query, args: args, t: t, cancel: cancel}
}
// Close closes the statement.
func (s *Stmt) Close() (err error) {
stmt, ok := s.stmt.Load().(*sql.Stmt)
if ok {
err = errors.WithStack(stmt.Close())
}
return
}
func (s *Stmt) prepare() (st *sql.Stmt) {
var ok bool
if st, ok = s.stmt.Load().(*sql.Stmt); ok {
return
}
var err error
if st, err = s.db.Prepare(s.query); err == nil {
s.stmt.Store(st)
}
return
}
// Exec executes a prepared statement with the given arguments and returns a
// Result summarizing the effect of the statement.
func (s *Stmt) Exec(c context.Context, args ...interface{}) (res sql.Result, err error) {
now := time.Now()
if s.tx {
if s.t != nil {
s.t.SetTag(trace.String(trace.TagAnnotation, s.query))
}
} else if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "exec")
t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query))
defer t.Finish(&err)
}
if err = s.db.breaker.Allow(); err != nil {
stats.Incr("tidb:stmt:exec", "breaker")
return
}
stmt := s.prepare()
if stmt == nil {
err = ErrStmtNil
return
}
_, c, cancel := s.db.conf.ExecTimeout.Shrink(c)
res, err = stmt.ExecContext(c, args...)
cancel()
s.db.onBreaker(&err)
stats.Timing("tidb:stmt:exec", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "addr: %s exec:%s, args:%+v", s.db.addr, s.query, args)
}
return
}
// Query executes a prepared query statement with the given arguments and
// returns the query results as a *Rows.
func (s *Stmt) Query(c context.Context, args ...interface{}) (rows *Rows, err error) {
if s.tx {
if s.t != nil {
s.t.SetTag(trace.String(trace.TagAnnotation, s.query))
}
} else if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "query")
t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query))
defer t.Finish(&err)
}
if err = s.db.breaker.Allow(); err != nil {
stats.Incr("tidb:stmt:query", "breaker")
return
}
stmt := s.prepare()
if stmt == nil {
err = ErrStmtNil
return
}
now := time.Now()
_, c, cancel := s.db.conf.QueryTimeout.Shrink(c)
rs, err := stmt.QueryContext(c, args...)
s.db.onBreaker(&err)
stats.Timing("tidb:stmt:query", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "addr: %s, query:%s, args:%+v", s.db.addr, s.query, args)
cancel()
return
}
rows = &Rows{Rows: rs, cancel: cancel}
return
}
// QueryRow executes a prepared query statement with the given arguments.
// If an error occurs during the execution of the statement, that error will
// be returned by a call to Scan on the returned *Row, which is always non-nil.
// If the query selects no rows, the *Row's Scan will return ErrNoRows.
// Otherwise, the *Row's Scan scans the first selected row and discards the rest.
func (s *Stmt) QueryRow(c context.Context, args ...interface{}) (row *Row) {
now := time.Now()
row = &Row{db: s.db, query: s.query, args: args}
if s.tx {
if s.t != nil {
s.t.SetTag(trace.String(trace.TagAnnotation, s.query))
}
} else if t, ok := trace.FromContext(c); ok {
t = t.Fork(_family, "queryrow")
t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query))
row.t = t
}
if row.err = s.db.breaker.Allow(); row.err != nil {
stats.Incr("tidb:stmt:queryrow", "breaker")
return
}
stmt := s.prepare()
if stmt == nil {
return
}
_, c, cancel := s.db.conf.QueryTimeout.Shrink(c)
row.Row = stmt.QueryRowContext(c, args...)
row.cancel = cancel
stats.Timing("tidb:stmt:queryrow", int64(time.Since(now)/time.Millisecond))
return
}
func (s *Stmts) prepare(conn *conn) (st *Stmt) {
if conn == nil {
conn = s.db.conn()
}
s.mu.RLock()
st = s.sts[conn.addr]
s.mu.RUnlock()
if st == nil {
st = conn.prepared(s.query)
s.mu.Lock()
s.sts[conn.addr] = st
s.mu.Unlock()
}
return
}
// Exec executes a prepared statement with the given arguments and returns a
// Result summarizing the effect of the statement.
func (s *Stmts) Exec(c context.Context, args ...interface{}) (res sql.Result, err error) {
return s.prepare(nil).Exec(c, args...)
}
// Query executes a prepared query statement with the given arguments and
// returns the query results as a *Rows.
func (s *Stmts) Query(c context.Context, args ...interface{}) (rows *Rows, err error) {
return s.prepare(nil).Query(c, args...)
}
// QueryRow executes a prepared query statement with the given arguments.
// If an error occurs during the execution of the statement, that error will
// be returned by a call to Scan on the returned *Row, which is always non-nil.
// If the query selects no rows, the *Row's Scan will return ErrNoRows.
// Otherwise, the *Row's Scan scans the first selected row and discards the rest.
func (s *Stmts) QueryRow(c context.Context, args ...interface{}) (row *Row) {
return s.prepare(nil).QueryRow(c, args...)
}
// Close closes the statement.
func (s *Stmts) Close() (err error) {
for _, st := range s.sts {
if err = errors.WithStack(st.Close()); err != nil {
return
}
}
return
}
// Commit commits the transaction.
func (tx *Tx) Commit() (err error) {
err = tx.tx.Commit()
tx.cancel()
tx.db.onBreaker(&err)
if tx.t != nil {
tx.t.Finish(&err)
}
if err != nil {
err = errors.WithStack(err)
}
return
}
// Rollback aborts the transaction.
func (tx *Tx) Rollback() (err error) {
err = tx.tx.Rollback()
tx.cancel()
tx.db.onBreaker(&err)
if tx.t != nil {
tx.t.Finish(&err)
}
if err != nil {
err = errors.WithStack(err)
}
return
}
// Exec executes a query that doesn't return rows. For example: an INSERT and
// UPDATE.
func (tx *Tx) Exec(query string, args ...interface{}) (res sql.Result, err error) {
now := time.Now()
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("exec %s", query)))
}
res, err = tx.tx.ExecContext(tx.c, query, args...)
stats.Timing("tidb:tx:exec", int64(time.Since(now)/time.Millisecond))
if err != nil {
err = errors.Wrapf(err, "addr: %s exec:%s, args:%+v", tx.db.addr, query, args)
}
return
}
// Query executes a query that returns rows, typically a SELECT.
func (tx *Tx) Query(query string, args ...interface{}) (rows *Rows, err error) {
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("query %s", query)))
}
now := time.Now()
defer func() {
stats.Timing("tidb:tx:query", int64(time.Since(now)/time.Millisecond))
}()
rs, err := tx.tx.QueryContext(tx.c, query, args...)
if err == nil {
rows = &Rows{Rows: rs}
} else {
err = errors.Wrapf(err, "addr: %s, query:%s, args:%+v", tx.db.addr, query, args)
}
return
}
// QueryRow executes a query that is expected to return at most one row.
// QueryRow always returns a non-nil value. Errors are deferred until Row's
// Scan method is called.
func (tx *Tx) QueryRow(query string, args ...interface{}) *Row {
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("queryrow %s", query)))
}
now := time.Now()
defer func() {
stats.Timing("tidb:tx:queryrow", int64(time.Since(now)/time.Millisecond))
}()
r := tx.tx.QueryRowContext(tx.c, query, args...)
return &Row{Row: r, db: tx.db, query: query, args: args}
}
// Stmt returns a transaction-specific prepared statement from an existing statement.
func (tx *Tx) Stmt(stmt *Stmt) *Stmt {
if stmt == nil {
return nil
}
as, ok := stmt.stmt.Load().(*sql.Stmt)
if !ok {
return nil
}
ts := tx.tx.StmtContext(tx.c, as)
st := &Stmt{query: stmt.query, tx: true, t: tx.t, db: tx.db}
st.stmt.Store(ts)
return st
}
// Stmts returns a transaction-specific prepared statement from an existing statement.
func (tx *Tx) Stmts(stmt *Stmts) *Stmt {
return tx.Stmt(stmt.prepare(tx.db))
}
// Prepare creates a prepared statement for use within a transaction.
// The returned statement operates within the transaction and can no longer be
// used once the transaction has been committed or rolled back.
// To use an existing prepared statement on this transaction, see Tx.Stmt.
func (tx *Tx) Prepare(query string) (*Stmt, error) {
if tx.t != nil {
tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("prepare %s", query)))
}
stmt, err := tx.tx.Prepare(query)
if err != nil {
err = errors.Wrapf(err, "addr: %s prepare %s", tx.db.addr, query)
return nil, err
}
st := &Stmt{query: query, tx: true, t: tx.t, db: tx.db}
st.stmt.Store(stmt)
return st, nil
}
// parseDSNAddr parse dsn name and return addr.
func parseDSNAddr(dsn string) (addr string) {
if dsn == "" {
return
}
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
return
}
addr = cfg.Addr
return
}
func genDSN(dsn, addr string) (res string) {
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
return
}
cfg.Addr = addr
cfg.Net = "tcp"
res = cfg.FormatDSN()
return
}

View File

@@ -0,0 +1,21 @@
package tidb
import "testing"
func TestParseDSNAddrr(t *testing.T) {
dsn := "u:p@tcp(127.0.0.1:3306)/db?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
addr := parseDSNAddr(dsn)
if addr != "127.0.0.1:3306" {
t.Errorf("expect 127.0.0.1:3306 got: %s", addr)
}
}
func TestGenDSN(t *testing.T) {
dsn := "u:p@tcp(127.0.0.1:3306)/db?loc=Local&parseTime=true&readTimeout=5s&timeout=5s&writeTimeout=5s&charset=utf8mb4"
addr := "127.0.0.2:3308"
expect := "u:p@tcp(127.0.0.2:3308)/db?loc=Local&parseTime=true&readTimeout=5s&timeout=5s&writeTimeout=5s&charset=utf8mb4"
newdsn := genDSN(dsn, addr)
if newdsn != expect {
t.Errorf("expect %s got: %s", expect, newdsn)
}
}

View File

@@ -0,0 +1,38 @@
package tidb
import (
"go-common/library/log"
"go-common/library/net/netutil/breaker"
"go-common/library/stat"
"go-common/library/time"
// database driver
_ "github.com/go-sql-driver/mysql"
)
var stats = stat.DB
// Config mysql config.
type Config struct {
DSN string // dsn
Active int // pool
Idle int // pool
IdleTimeout time.Duration // connect max life time.
QueryTimeout time.Duration // query sql timeout
ExecTimeout time.Duration // execute sql timeout
TranTimeout time.Duration // transaction sql timeout
Breaker *breaker.Config // breaker
}
// NewTiDB new db and retry connection when has error.
func NewTiDB(c *Config) (db *DB) {
if c.QueryTimeout == 0 || c.ExecTimeout == 0 || c.TranTimeout == 0 {
panic("tidb must be set query/execute/transction timeout")
}
db, err := Open(c)
if err != nil {
log.Error("open tidb error(%v)", err)
panic(err)
}
return
}

View File

@@ -0,0 +1,234 @@
package tidb
import (
"context"
"database/sql"
"os"
"testing"
"time"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
)
func TestMySQL(t *testing.T) {
bc := &breaker.Config{
Window: xtime.Duration(10 * time.Second),
Sleep: xtime.Duration(10 * time.Second),
Bucket: 10,
Ratio: 0.5,
Request: 100,
}
dsn := os.Getenv("TEST_MYSQL_DSN")
if dsn == "" {
t.Skipf("TEST_MYSQL_DSN is empty, sql test skipped")
}
dsn = dsn + "?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8"
c := &Config{
DSN: dsn,
Active: 10,
Idle: 5,
IdleTimeout: xtime.Duration(time.Minute),
QueryTimeout: xtime.Duration(time.Minute),
ExecTimeout: xtime.Duration(time.Minute),
TranTimeout: xtime.Duration(time.Minute),
Breaker: bc,
}
db := NewTiDB(c)
defer db.Close()
testPing(t, db)
testTable(t, db)
testExec(t, db)
testQuery(t, db)
testQueryRow(t, db)
testPrepare(t, db)
testPrepared(t, db)
testTransaction(t, db)
}
func testTransaction(t *testing.T, db *DB) {
var (
tx *Tx
err error
execSQL = "INSERT INTO test(name) VALUES(?)"
selSQL = "SELECT name FROM test WHERE name=?"
txstmt *Stmt
)
if tx, err = db.Begin(context.TODO()); err != nil {
t.Errorf("MySQL: db transaction Begin err(%v)", err)
tx.Rollback()
return
}
t.Log("MySQL: db transaction begin")
if txstmt, err = tx.Prepare(execSQL); err != nil {
t.Errorf("MySQL: tx.Prepare err(%v)", err)
}
if stmt := tx.Stmt(txstmt); stmt == nil {
t.Errorf("MySQL:tx.Stmt err(%v)", err)
}
// exec
if _, err = tx.Exec(execSQL, "tx1"); err != nil {
t.Errorf("MySQL: tx.Exec err(%v)", err)
tx.Rollback()
return
}
t.Logf("MySQL:tx.Exec tx1")
if _, err = tx.Exec(execSQL, "tx1"); err != nil {
t.Errorf("MySQL: tx.Exec err(%v)", err)
tx.Rollback()
return
}
t.Logf("MySQL:tx.Exec tx1")
// query
rows, err := tx.Query(selSQL, "tx2")
if err != nil {
t.Errorf("MySQL:tx.Query err(%v)", err)
tx.Rollback()
return
}
rows.Close()
t.Log("MySQL: tx.Query tx2")
// queryrow
var name string
row := tx.QueryRow(selSQL, "noexist")
if err = row.Scan(&name); err != sql.ErrNoRows {
t.Errorf("MySQL: queryRow name: noexist")
}
if err = tx.Commit(); err != nil {
t.Errorf("MySQL:tx.Commit err(%v)", err)
}
if err = tx.Commit(); err != nil {
t.Logf("MySQL:tx.Commit err(%v)", err)
}
if err = tx.Rollback(); err != nil {
t.Logf("MySQL:tx Rollback err(%v)", err)
}
}
func testPing(t *testing.T, db *DB) {
if err := db.Ping(context.TODO()); err != nil {
t.Errorf("MySQL: ping error(%v)", err)
t.FailNow()
} else {
t.Log("MySQL: ping ok")
}
}
func testTable(t *testing.T, db *DB) {
table := "CREATE TABLE IF NOT EXISTS `test` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID', `name` varchar(16) NOT NULL DEFAULT '' COMMENT '名称', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8"
if _, err := db.Exec(context.TODO(), table); err != nil {
t.Errorf("MySQL: create table error(%v)", err)
} else {
t.Log("MySQL: create table ok")
}
}
func testExec(t *testing.T, db *DB) {
sql := "INSERT INTO test(name) VALUES(?)"
if _, err := db.Exec(context.TODO(), sql, "test"); err != nil {
t.Errorf("MySQL: insert error(%v)", err)
} else {
t.Log("MySQL: insert ok")
}
}
func testQuery(t *testing.T, db *DB) {
sql := "SELECT name FROM test WHERE name=?"
rows, err := db.Query(context.TODO(), sql, "test")
if err != nil {
t.Errorf("MySQL: query error(%v)", err)
}
defer rows.Close()
for rows.Next() {
name := ""
if err := rows.Scan(&name); err != nil {
t.Errorf("MySQL: query scan error(%v)", err)
} else {
t.Logf("MySQL: query name: %s", name)
}
}
}
func testQueryRow(t *testing.T, db *DB) {
sql := "SELECT name FROM test WHERE name=?"
name := ""
row := db.QueryRow(context.TODO(), sql, "test")
if err := row.Scan(&name); err != nil {
t.Errorf("MySQL: queryRow error(%v)", err)
} else {
t.Logf("MySQL: queryRow name: %s", name)
}
}
func testPrepared(t *testing.T, db *DB) {
sql := "SELECT name FROM test WHERE name=?"
name := ""
stmt := db.Prepared(sql)
row := stmt.QueryRow(context.TODO(), "test")
if err := row.Scan(&name); err != nil {
t.Errorf("MySQL: prepared query error(%v)", err)
} else {
t.Logf("MySQL: prepared query name: %s", name)
}
if err := stmt.Close(); err != nil {
t.Errorf("MySQL:stmt.Close err(%v)", err)
}
}
func testPrepare(t *testing.T, db *DB) {
var (
selsql = "SELECT name FROM test WHERE name=?"
execsql = "INSERT INTO test(name) VALUES(?)"
name = ""
)
selstmt, err := db.Prepare(selsql)
if err != nil {
t.Errorf("MySQL:Prepare err(%v)", err)
return
}
row := selstmt.QueryRow(context.TODO(), "noexit")
if err = row.Scan(&name); err == sql.ErrNoRows {
t.Logf("MySQL: prepare query error(%v)", err)
} else {
t.Errorf("MySQL: prepared query name: noexist")
}
rows, err := selstmt.Query(context.TODO(), "test")
if err != nil {
t.Errorf("MySQL:stmt.Query err(%v)", err)
}
rows.Close()
execstmt, err := db.Prepare(execsql)
if err != nil {
t.Errorf("MySQL:Prepare err(%v)", err)
return
}
if _, err := execstmt.Exec(context.TODO(), "test"); err != nil {
t.Errorf("MySQL: stmt.Exec(%v)", err)
}
}
func BenchmarkMySQL(b *testing.B) {
c := &Config{
DSN: "test:test@tcp(172.16.0.148:3306)/test?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8",
Active: 10,
Idle: 5,
IdleTimeout: xtime.Duration(time.Minute),
}
db := NewTiDB(c)
defer db.Close()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
sql := "SELECT name FROM test WHERE name=?"
rows, err := db.Query(context.TODO(), sql, "test")
if err == nil {
for rows.Next() {
var name string
if err = rows.Scan(&name); err != nil {
break
}
}
rows.Close()
}
}
})
}