mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
Bump Tarantool client library from 2.4.2 to 3.0.0 (#10377)
* Bump Tarantool client library from 2.4.2 to 3.0.0 * Fix review comments --------- Co-authored-by: Marat Karimov <karimov_m@inbox.ru>
This commit is contained in:
co-authored by
Marat Karimov
parent
875cd1f67e
commit
ed1c54dae9
@@ -15,3 +15,9 @@ COPY tarantool /opt/tarantool/app
|
|||||||
# build app
|
# build app
|
||||||
RUN tt build app
|
RUN tt build app
|
||||||
|
|
||||||
|
# the base image's default healthcheck assumes a single-instance layout and
|
||||||
|
# checks a control socket path that doesn't exist for this multi-instance
|
||||||
|
# app, so it never reports healthy; check instance status instead.
|
||||||
|
HEALTHCHECK --interval=5s --timeout=3s --start-period=15s --retries=6 \
|
||||||
|
CMD tt status app | awk 'NR>2{if ($2!="RUNNING") exit 1} END{if (NR<=2) exit 1}'
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ require (
|
|||||||
github.com/schollz/progressbar/v3 v3.19.1
|
github.com/schollz/progressbar/v3 v3.19.1
|
||||||
github.com/seaweedfs/go-fuse/v2 v2.9.3
|
github.com/seaweedfs/go-fuse/v2 v2.9.3
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5
|
github.com/shirou/gopsutil/v4 v4.26.5
|
||||||
github.com/tarantool/go-tarantool/v2 v2.4.2
|
github.com/tarantool/go-option v1.1.0
|
||||||
|
github.com/tarantool/go-tarantool/v3 v3.0.0
|
||||||
github.com/testcontainers/testcontainers-go v0.43.0
|
github.com/testcontainers/testcontainers-go v0.43.0
|
||||||
github.com/tikv/client-go/v2 v2.0.7
|
github.com/tikv/client-go/v2 v2.0.7
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
github.com/xeipuuv/gojsonschema v1.2.0
|
||||||
|
|||||||
@@ -1905,8 +1905,10 @@ github.com/t3rm1n4l/go-mega v0.0.0-20251120131202-6845944c051c/go.mod h1:BF/l2jN
|
|||||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||||
github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A=
|
github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A=
|
||||||
github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo=
|
github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo=
|
||||||
github.com/tarantool/go-tarantool/v2 v2.4.2 h1:rkzYtFhLJLA9RDIhjzN93MJBN5PBxHW4+soq+RB90gE=
|
github.com/tarantool/go-option v1.1.0 h1:ShoOhNsdL41sRpm4hXCRDjV8H0WzPkd4UnKhLKbW//w=
|
||||||
github.com/tarantool/go-tarantool/v2 v2.4.2/go.mod h1:MTbhdjFc3Jl63Lgi/UJr5D+QbT+QegqOzsNJGmaw7VM=
|
github.com/tarantool/go-option v1.1.0/go.mod h1:hMr9z2JXOWlgdCBpCPSL2nwp8718GKYvNBJ+ZuzJbCo=
|
||||||
|
github.com/tarantool/go-tarantool/v3 v3.0.0 h1:zsIXS4nvSSXqZWtN/f1Bfuq973j6J85O5U/8RYQCRcE=
|
||||||
|
github.com/tarantool/go-tarantool/v3 v3.0.0/go.mod h1:TXxLWhUCgdxXFfelnTSkq+goKRTRj660zxq4/WXPe8k=
|
||||||
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
|
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
|
||||||
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
|
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
|
||||||
github.com/testcontainers/testcontainers-go/modules/compose v0.42.0 h1:+t1ZN31TD36cwxmeLqGwe7wIdvblBm0Z+vlj4SX8Mv0=
|
github.com/testcontainers/testcontainers-go/modules/compose v0.42.0 h1:+t1ZN31TD36cwxmeLqGwe7wIdvblBm0Z+vlj4SX8Mv0=
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//go:build tarantool
|
||||||
|
|
||||||
|
package tarantool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// glogSlogHandler forwards go-tarantool/v3's structured logs into weed/glog,
|
||||||
|
// which is the logging facility used everywhere else in SeaweedFS.
|
||||||
|
type glogSlogHandler struct {
|
||||||
|
preformatted string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGlogLogger() *slog.Logger {
|
||||||
|
return slog.New(&glogSlogHandler{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *glogSlogHandler) Enabled(context.Context, slog.Level) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *glogSlogHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
msg := r.Message
|
||||||
|
var attrs []string
|
||||||
|
if h.preformatted != "" {
|
||||||
|
attrs = append(attrs, h.preformatted)
|
||||||
|
}
|
||||||
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
|
attrs = append(attrs, fmt.Sprintf("%s=%v", a.Key, a.Value.Any()))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if len(attrs) > 0 {
|
||||||
|
msg = msg + " " + strings.Join(attrs, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.Level >= slog.LevelError:
|
||||||
|
glog.ErrorfCtx(ctx, "tarantool: %s", msg)
|
||||||
|
case r.Level >= slog.LevelWarn:
|
||||||
|
glog.WarningfCtx(ctx, "tarantool: %s", msg)
|
||||||
|
case r.Level >= slog.LevelInfo:
|
||||||
|
glog.InfofCtx(ctx, "tarantool: %s", msg)
|
||||||
|
default:
|
||||||
|
glog.V(1).InfofCtx(ctx, "tarantool: %s", msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *glogSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
if h.preformatted != "" {
|
||||||
|
sb.WriteString(h.preformatted)
|
||||||
|
sb.WriteString(" ")
|
||||||
|
}
|
||||||
|
for i, a := range attrs {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%s=%v", a.Key, a.Value.Any()))
|
||||||
|
}
|
||||||
|
return &glogSlogHandler{preformatted: sb.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *glogSlogHandler) WithGroup(name string) slog.Handler {
|
||||||
|
return h
|
||||||
|
}
|
||||||
@@ -14,9 +14,10 @@ import (
|
|||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||||
weed_util "github.com/seaweedfs/seaweedfs/weed/util"
|
weed_util "github.com/seaweedfs/seaweedfs/weed/util"
|
||||||
"github.com/tarantool/go-tarantool/v2"
|
"github.com/tarantool/go-option"
|
||||||
"github.com/tarantool/go-tarantool/v2/crud"
|
"github.com/tarantool/go-tarantool/v3"
|
||||||
"github.com/tarantool/go-tarantool/v2/pool"
|
"github.com/tarantool/go-tarantool/v3/crud"
|
||||||
|
"github.com/tarantool/go-tarantool/v3/pool"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -28,7 +29,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TarantoolStore struct {
|
type TarantoolStore struct {
|
||||||
pool *pool.ConnectionPool
|
pool *pool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *TarantoolStore) GetName() string {
|
func (store *TarantoolStore) GetName() string {
|
||||||
@@ -53,36 +54,34 @@ func (store *TarantoolStore) Initialize(configuration weed_util.Configuration, p
|
|||||||
return fmt.Errorf("parse tarantool store timeout: %w", err)
|
return fmt.Errorf("parse tarantool store timeout: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxReconnects := configuration.GetInt(prefix + "maxReconnects")
|
if maxReconnects := configuration.GetInt(prefix + "maxReconnects"); maxReconnects != 1000 {
|
||||||
if maxReconnects < 0 {
|
glog.Warningf("tarantool store: \"maxReconnects\" is no longer supported with go-tarantool v3 (the connection pool manages reconnection internally); ignoring configured value %d", maxReconnects)
|
||||||
return fmt.Errorf("maxReconnects is negative")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses := strings.Split(address, ",")
|
addresses := strings.Split(address, ",")
|
||||||
|
|
||||||
return store.initialize(addresses, user, password, timeout, uint(maxReconnects))
|
return store.initialize(addresses, user, password, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *TarantoolStore) initialize(addresses []string, user string, password string, timeout time.Duration, maxReconnects uint) error {
|
func (store *TarantoolStore) initialize(addresses []string, user string, password string, timeout time.Duration) error {
|
||||||
|
|
||||||
opts := tarantool.Opts{
|
opts := tarantool.Opts{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
Reconnect: time.Second,
|
|
||||||
MaxReconnects: maxReconnects,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
poolInstances := makePoolInstances(addresses, user, password, opts)
|
poolInstances := makePoolInstances(addresses, user, password, opts)
|
||||||
poolOpts := pool.Opts{
|
poolOpts := pool.Opts{
|
||||||
CheckTimeout: time.Second,
|
CheckTimeout: time.Second,
|
||||||
|
Logger: newGlogLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
p, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts)
|
p, err := pool.NewWithOpts(ctx, poolInstances, poolOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Can't create connection pool: %w", err)
|
return fmt.Errorf("Can't create connection pool: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = p.Do(tarantool.NewPingRequest(), pool.ANY).Get()
|
_, err = p.Do(tarantool.NewPingRequest(), pool.ModeAny).Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,13 +149,13 @@ func (store *TarantoolStore) InsertEntry(ctx context.Context, entry *filer.Entry
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
req := crud.MakeUpsertRequest(tarantoolSpaceName).
|
req := crud.NewUpsertRequest(tarantoolSpaceName).
|
||||||
Tuple([]interface{}{dir, nil, name, ttl, string(meta)}).
|
Tuple([]interface{}{dir, nil, name, ttl, string(meta)}).
|
||||||
Operations(operations)
|
Operations(operations)
|
||||||
|
|
||||||
ret := crud.Result{}
|
ret := crud.Result{}
|
||||||
|
|
||||||
if err := store.pool.Do(req, pool.RW).GetTyped(&ret); err != nil {
|
if err := store.pool.Do(req, pool.ModeRW).GetTyped(&ret); err != nil {
|
||||||
return fmt.Errorf("insert %s: %s", entry.FullPath, err)
|
return fmt.Errorf("insert %s: %s", entry.FullPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +170,19 @@ func (store *TarantoolStore) FindEntry(ctx context.Context, fullpath weed_util.F
|
|||||||
dir, name := fullpath.DirAndName()
|
dir, name := fullpath.DirAndName()
|
||||||
|
|
||||||
findEntryGetOpts := crud.GetOpts{
|
findEntryGetOpts := crud.GetOpts{
|
||||||
Fields: crud.MakeOptTuple([]interface{}{"data"}),
|
Fields: option.SomeAny([]interface{}{"data"}),
|
||||||
Mode: crud.MakeOptString("read"),
|
Mode: option.SomeString("read"),
|
||||||
PreferReplica: crud.MakeOptBool(true),
|
PreferReplica: option.SomeBool(true),
|
||||||
Balance: crud.MakeOptBool(true),
|
Balance: option.SomeBool(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := crud.MakeGetRequest(tarantoolSpaceName).
|
req := crud.NewGetRequest(tarantoolSpaceName).
|
||||||
Key(crud.Tuple([]interface{}{dir, name})).
|
Key([]interface{}{dir, name}).
|
||||||
Opts(findEntryGetOpts)
|
Opts(findEntryGetOpts)
|
||||||
|
|
||||||
resp := crud.Result{}
|
resp := crud.Result{}
|
||||||
|
|
||||||
err = store.pool.Do(req, pool.PreferRO).GetTyped(&resp)
|
err = store.pool.Do(req, pool.ModePreferRO).GetTyped(&resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -219,14 +218,14 @@ func (store *TarantoolStore) DeleteEntry(ctx context.Context, fullpath weed_util
|
|||||||
dir, name := fullpath.DirAndName()
|
dir, name := fullpath.DirAndName()
|
||||||
|
|
||||||
delOpts := crud.DeleteOpts{
|
delOpts := crud.DeleteOpts{
|
||||||
Noreturn: crud.MakeOptBool(true),
|
Noreturn: option.SomeBool(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := crud.MakeDeleteRequest(tarantoolSpaceName).
|
req := crud.NewDeleteRequest(tarantoolSpaceName).
|
||||||
Key(crud.Tuple([]interface{}{dir, name})).
|
Key([]interface{}{dir, name}).
|
||||||
Opts(delOpts)
|
Opts(delOpts)
|
||||||
|
|
||||||
if _, err := store.pool.Do(req, pool.RW).Get(); err != nil {
|
if _, err := store.pool.Do(req, pool.ModeRW).Get(); err != nil {
|
||||||
return fmt.Errorf("delete %s : %v", fullpath, err)
|
return fmt.Errorf("delete %s : %v", fullpath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +236,7 @@ func (store *TarantoolStore) DeleteFolderChildren(ctx context.Context, fullpath
|
|||||||
req := tarantool.NewCallRequest("filer_metadata.delete_by_directory_idx").
|
req := tarantool.NewCallRequest("filer_metadata.delete_by_directory_idx").
|
||||||
Args([]interface{}{fullpath})
|
Args([]interface{}{fullpath})
|
||||||
|
|
||||||
if _, err := store.pool.Do(req, pool.RW).Get(); err != nil {
|
if _, err := store.pool.Do(req, pool.ModeRW).Get(); err != nil {
|
||||||
return fmt.Errorf("delete %s : %v", fullpath, err)
|
return fmt.Errorf("delete %s : %v", fullpath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +252,7 @@ func (store *TarantoolStore) ListDirectoryEntries(ctx context.Context, dirPath w
|
|||||||
req := tarantool.NewCallRequest("filer_metadata.find_by_directory_idx_and_name").
|
req := tarantool.NewCallRequest("filer_metadata.find_by_directory_idx_and_name").
|
||||||
Args([]interface{}{string(dirPath), startFileName, includeStartFile, limit})
|
Args([]interface{}{string(dirPath), startFileName, includeStartFile, limit})
|
||||||
|
|
||||||
results, err := store.pool.Do(req, pool.PreferRO).Get()
|
results, err := store.pool.Do(req, pool.ModePreferRO).Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||||
"github.com/tarantool/go-tarantool/v2/crud"
|
"github.com/tarantool/go-option"
|
||||||
"github.com/tarantool/go-tarantool/v2/pool"
|
"github.com/tarantool/go-tarantool/v3/crud"
|
||||||
|
"github.com/tarantool/go-tarantool/v3/pool"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,12 +27,12 @@ func (store *TarantoolStore) KvPut(ctx context.Context, key []byte, value []byte
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
req := crud.MakeUpsertRequest(tarantoolKVSpaceName).
|
req := crud.NewUpsertRequest(tarantoolKVSpaceName).
|
||||||
Tuple([]interface{}{string(key), nil, string(value)}).
|
Tuple([]interface{}{string(key), nil, string(value)}).
|
||||||
Operations(operations)
|
Operations(operations)
|
||||||
|
|
||||||
ret := crud.Result{}
|
ret := crud.Result{}
|
||||||
if err := store.pool.Do(req, pool.RW).GetTyped(&ret); err != nil {
|
if err := store.pool.Do(req, pool.ModeRW).GetTyped(&ret); err != nil {
|
||||||
return fmt.Errorf("kv put: %w", err)
|
return fmt.Errorf("kv put: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,19 +42,19 @@ func (store *TarantoolStore) KvPut(ctx context.Context, key []byte, value []byte
|
|||||||
func (store *TarantoolStore) KvGet(ctx context.Context, key []byte) (value []byte, err error) {
|
func (store *TarantoolStore) KvGet(ctx context.Context, key []byte) (value []byte, err error) {
|
||||||
|
|
||||||
getOpts := crud.GetOpts{
|
getOpts := crud.GetOpts{
|
||||||
Fields: crud.MakeOptTuple([]interface{}{"value"}),
|
Fields: option.SomeAny([]interface{}{"value"}),
|
||||||
Mode: crud.MakeOptString("read"),
|
Mode: option.SomeString("read"),
|
||||||
PreferReplica: crud.MakeOptBool(true),
|
PreferReplica: option.SomeBool(true),
|
||||||
Balance: crud.MakeOptBool(true),
|
Balance: option.SomeBool(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := crud.MakeGetRequest(tarantoolKVSpaceName).
|
req := crud.NewGetRequest(tarantoolKVSpaceName).
|
||||||
Key(crud.Tuple([]interface{}{string(key)})).
|
Key([]interface{}{string(key)}).
|
||||||
Opts(getOpts)
|
Opts(getOpts)
|
||||||
|
|
||||||
resp := crud.Result{}
|
resp := crud.Result{}
|
||||||
|
|
||||||
err = store.pool.Do(req, pool.PreferRO).GetTyped(&resp)
|
err = store.pool.Do(req, pool.ModePreferRO).GetTyped(&resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -79,14 +80,14 @@ func (store *TarantoolStore) KvGet(ctx context.Context, key []byte) (value []byt
|
|||||||
func (store *TarantoolStore) KvDelete(ctx context.Context, key []byte) (err error) {
|
func (store *TarantoolStore) KvDelete(ctx context.Context, key []byte) (err error) {
|
||||||
|
|
||||||
delOpts := crud.DeleteOpts{
|
delOpts := crud.DeleteOpts{
|
||||||
Noreturn: crud.MakeOptBool(true),
|
Noreturn: option.SomeBool(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := crud.MakeDeleteRequest(tarantoolKVSpaceName).
|
req := crud.NewDeleteRequest(tarantoolKVSpaceName).
|
||||||
Key(crud.Tuple([]interface{}{string(key)})).
|
Key([]interface{}{string(key)}).
|
||||||
Opts(delOpts)
|
Opts(delOpts)
|
||||||
|
|
||||||
if _, err := store.pool.Do(req, pool.RW).Get(); err != nil {
|
if _, err := store.pool.Do(req, pool.ModeRW).Get(); err != nil {
|
||||||
return fmt.Errorf("kv delete: %w", err)
|
return fmt.Errorf("kv delete: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ func TestStore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
store := &TarantoolStore{}
|
store := &TarantoolStore{}
|
||||||
addresses := []string{"127.0.1:3303"}
|
addresses := []string{"127.0.1:3303"}
|
||||||
store.initialize(addresses, "client", "client", 5*time.Second, 1000)
|
store.initialize(addresses, "client", "client", 5*time.Second)
|
||||||
store_test.TestFilerStore(t, store)
|
store_test.TestFilerStore(t, store)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user