diff --git a/code/go/0chain.net/blobber/config.go b/code/go/0chain.net/blobber/config.go index 2ac2c85ef..3afef74b2 100644 --- a/code/go/0chain.net/blobber/config.go +++ b/code/go/0chain.net/blobber/config.go @@ -73,6 +73,8 @@ func setupConfig() { viper.GetDuration("write_lock_timeout") / time.Second, ) + config.Configuration.WriteMarkerLockTimeout = viper.GetDuration("write_marker_lock_timeout") + config.Configuration.UpdateAllocationsInterval = viper.GetDuration("update_allocations_interval") diff --git a/code/go/0chain.net/blobbercore/allocation/dao.go b/code/go/0chain.net/blobbercore/allocation/dao.go new file mode 100644 index 000000000..cc11aa989 --- /dev/null +++ b/code/go/0chain.net/blobbercore/allocation/dao.go @@ -0,0 +1,53 @@ +package allocation + +import ( + "context" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + "github.com/0chain/blobber/code/go/0chain.net/core/common" + "github.com/0chain/errors" + "github.com/0chain/gosdk/constants" + "gorm.io/gorm" +) + +// GetOrCreate, get allocation if it exists in db. if not, try to sync it from blockchain, and insert it in db. +func GetOrCreate(ctx context.Context, store datastore.Store, allocationTx string) (*Allocation, error) { + db := store.GetDB() + + if len(allocationTx) == 0 { + return nil, errors.Throw(constants.ErrInvalidParameter, "tx") + } + + alloc := &Allocation{} + result := db.Table(TableNameAllocation).Where(SQLWhereGetByTx, allocationTx).First(alloc) + + if result.Error == nil { + return alloc, nil + } + + if !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, errors.ThrowLog(result.Error.Error(), common.ErrBadDataStore) + } + + return SyncAllocation(allocationTx) + +} + +const ( + SQLWhereGetByTx = "allocations.tx = ?" +) + +// DryRun Creates a prepared statement when executing any SQL and caches them to speed up future calls +// https://gorm.io/docs/performance.html#Caches-Prepared-Statement +func DryRun(db *gorm.DB) { + + // https://gorm.io/docs/session.html#DryRun + // Session mode + tx := db.Session(&gorm.Session{PrepareStmt: true, DryRun: true}) + + // use Table instead of Model to reduce reflect times + + // prepare statement for GetOrCreate + tx.Table(TableNameAllocation).Where(SQLWhereGetByTx, "tx").First(&Allocation{}) + +} diff --git a/code/go/0chain.net/blobbercore/allocation/entity.go b/code/go/0chain.net/blobbercore/allocation/entity.go index 0bf78f663..12b8794c1 100644 --- a/code/go/0chain.net/blobbercore/allocation/entity.go +++ b/code/go/0chain.net/blobbercore/allocation/entity.go @@ -18,6 +18,10 @@ const ( CHUNK_SIZE = 64 * KB ) +const ( + TableNameAllocation = "allocations" +) + type Allocation struct { ID string `gorm:"column:id;primary_key"` Tx string `gorm:"column:tx"` @@ -44,7 +48,7 @@ type Allocation struct { } func (Allocation) TableName() string { - return "allocations" + return TableNameAllocation } // RestDurationInTimeUnits returns number (float point) of time units until @@ -196,6 +200,10 @@ func (p *Pending) Save(tx *gorm.DB) error { return tx.Save(p).Error } +const ( + TableNameTerms = "terms" +) + // Terms for allocation by its Tx. type Terms struct { ID int64 `gorm:"column:id;primary_key"` @@ -207,7 +215,7 @@ type Terms struct { } func (*Terms) TableName() string { - return "terms" + return TableNameTerms } type ReadPool struct { diff --git a/code/go/0chain.net/blobbercore/allocation/zcn.go b/code/go/0chain.net/blobbercore/allocation/zcn.go new file mode 100644 index 000000000..bf7eb3778 --- /dev/null +++ b/code/go/0chain.net/blobbercore/allocation/zcn.go @@ -0,0 +1,87 @@ +package allocation + +import ( + "encoding/json" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + "github.com/0chain/blobber/code/go/0chain.net/core/chain" + "github.com/0chain/blobber/code/go/0chain.net/core/common" + "github.com/0chain/blobber/code/go/0chain.net/core/node" + "github.com/0chain/blobber/code/go/0chain.net/core/transaction" + "github.com/0chain/errors" + "gorm.io/gorm" +) + +// SyncAllocation try to pull allocation from blockchain, and insert it in db. +func SyncAllocation(allocationTx string) (*Allocation, error) { + t, err := transaction.VerifyTransaction(allocationTx, chain.GetServerChain()) + if err != nil { + return nil, errors.Throw(common.ErrBadRequest, + "Invalid Allocation id. Allocation not found in blockchain.") + } + var sa transaction.StorageAllocation + err = json.Unmarshal([]byte(t.TransactionOutput), &sa) + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrInternal, "Error decoding the allocation transaction output.") + } + + alloc := &Allocation{} + + belongToThisBlobber := false + for _, blobberConnection := range sa.Blobbers { + if blobberConnection.ID == node.Self.ID { + belongToThisBlobber = true + + alloc.AllocationRoot = "" + alloc.BlobberSize = (sa.Size + int64(len(sa.Blobbers)-1)) / + int64(len(sa.Blobbers)) + alloc.BlobberSizeUsed = 0 + + break + } + } + if !belongToThisBlobber { + return nil, errors.Throw(common.ErrBadRequest, + "Blobber is not part of the open connection transaction") + } + + // set/update fields + alloc.ID = sa.ID + alloc.Tx = sa.Tx + alloc.Expiration = sa.Expiration + alloc.OwnerID = sa.OwnerID + alloc.OwnerPublicKey = sa.OwnerPublicKey + alloc.RepairerID = t.ClientID // blobber node id + alloc.TotalSize = sa.Size + alloc.UsedSize = sa.UsedSize + alloc.Finalized = sa.Finalized + alloc.TimeUnit = sa.TimeUnit + alloc.IsImmutable = sa.IsImmutable + + // related terms + terms := make([]*Terms, 0, len(sa.BlobberDetails)) + for _, d := range sa.BlobberDetails { + terms = append(terms, &Terms{ + BlobberID: d.BlobberID, + AllocationID: alloc.ID, + ReadPrice: d.Terms.ReadPrice, + WritePrice: d.Terms.WritePrice, + }) + } + + err = datastore.GetStore().GetDB().Transaction(func(tx *gorm.DB) error { + if err := tx.Table(TableNameAllocation).Create(alloc).Error; err != nil { + return err + } + + for _, term := range terms { + if err := tx.Table(TableNameTerms).Create(term).Error; err != nil { + return err + } + } + + return nil + }) + + return alloc, err +} diff --git a/code/go/0chain.net/blobbercore/config/config.go b/code/go/0chain.net/blobbercore/config/config.go index c574c44eb..0d12680d4 100644 --- a/code/go/0chain.net/blobbercore/config/config.go +++ b/code/go/0chain.net/blobbercore/config/config.go @@ -33,6 +33,7 @@ func SetupDefaultConfig() { viper.SetDefault("challenge_completion_time", time.Duration(-1)) viper.SetDefault("read_lock_timeout", time.Duration(-1)) viper.SetDefault("write_lock_timeout", time.Duration(-1)) + viper.SetDefault("write_marker_lock_timeout", time.Second*30) viper.SetDefault("delegate_wallet", "") viper.SetDefault("min_stake", 1.0) @@ -115,6 +116,8 @@ type Config struct { ReadLockTimeout int64 // seconds WriteLockTimeout int64 // seconds + // WriteMarkerLockTimeout lock is released automatically if it is timeout + WriteMarkerLockTimeout time.Duration UpdateAllocationsInterval time.Duration diff --git a/code/go/0chain.net/blobbercore/datastore/postgres.go b/code/go/0chain.net/blobbercore/datastore/postgres.go index 71ec3b440..b622a3ba4 100644 --- a/code/go/0chain.net/blobbercore/datastore/postgres.go +++ b/code/go/0chain.net/blobbercore/datastore/postgres.go @@ -73,7 +73,7 @@ func (store *postgresStore) GetDB() *gorm.DB { func (store *postgresStore) AutoMigrate() error { - err := store.db.AutoMigrate(&Migration{}) + err := store.db.AutoMigrate(&Migration{}, &WriteLock{}) if err != nil { logging.Logger.Error("[db]", zap.Error(err)) } diff --git a/code/go/0chain.net/blobbercore/datastore/postgres_schema.go b/code/go/0chain.net/blobbercore/datastore/postgres_schema.go new file mode 100644 index 000000000..0ad6c3287 --- /dev/null +++ b/code/go/0chain.net/blobbercore/datastore/postgres_schema.go @@ -0,0 +1,19 @@ +package datastore + +import "time" + +const ( + TableNameWriteLock = "write_locks" +) + +// WriteLock WriteMarker lock +type WriteLock struct { + AllocationID string `gorm:"primaryKey, column:allocation_id"` + SessionID string `gorm:"column:session_id"` + CreatedAt time.Time `gorm:"column:created_at"` +} + +// TableName get table name of migrate +func (WriteLock) TableName() string { + return TableNameWriteLock +} diff --git a/code/go/0chain.net/blobbercore/handler/context.go b/code/go/0chain.net/blobbercore/handler/context.go new file mode 100644 index 000000000..2475b8527 --- /dev/null +++ b/code/go/0chain.net/blobbercore/handler/context.go @@ -0,0 +1,171 @@ +package handler + +import ( + "context" + "encoding/json" + "strconv" + "time" + + "net/http" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/allocation" + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + "github.com/0chain/blobber/code/go/0chain.net/core/common" + "github.com/0chain/errors" + "github.com/gorilla/mux" +) + +// Context api context +type Context struct { + context.Context + + // ClientID client wallet id + ClientID string + // ClientKey client wallet public key + ClientKey string + // AllocationTx optional. allcation id in request + AllocationTx string + // Signature optional. signature in request + Signature string + + Allocation *allocation.Allocation + + Store datastore.Store + Request *http.Request + + StatusCode int +} + +// FormValue get value from form data +func (c *Context) FormValue(key string) string { + return c.Request.FormValue(key) +} + +// FormTime get time from form data +func (c *Context) FormTime(key string) *time.Time { + value := c.Request.FormValue(key) + if len(value) == 0 { + return nil + } + + seconds, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil + } + + t := time.Unix(seconds, 0) + return &t +} + +type ErrorResponse struct { + Error string +} + +// WithHandler process handler to respond request +func WithHandler(handler func(ctx *Context) (interface{}, error)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS for all. + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "*") + return + } + + if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch { + ct := r.Header.Get("Content-Type") + if ct == "application/x-www-form-urlencoded" { + r.ParseForm() //nolint: errcheck + } else { + r.ParseMultipartForm(FormFileParseMaxMemory) //nolint: errcheck + } + + } + + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS for all. + w.Header().Set("Content-Type", "application/json") + + ctx, err := WithAuth(r) + statusCode := ctx.StatusCode + + if err != nil { + if statusCode == 0 { + statusCode = http.StatusInternalServerError + } + + buf, _ := json.Marshal(err) + http.Error(w, string(buf), statusCode) + return + } + + result, err := handler(ctx) + statusCode = ctx.StatusCode + + if err != nil { + if statusCode == 0 { + statusCode = http.StatusInternalServerError + } + + buf, _ := json.Marshal(err) + http.Error(w, string(buf), statusCode) + return + } + + if statusCode == 0 { + statusCode = http.StatusOK + } + w.WriteHeader(statusCode) + + if result != nil { + json.NewEncoder(w).Encode(result) //nolint + } + + } +} + +// WithAuth verify alloation and signature +func WithAuth(r *http.Request) (*Context, error) { + + ctx := &Context{ + Context: context.TODO(), + Request: r, + Store: datastore.GetStore(), + } + + var vars = mux.Vars(r) + + ctx.ClientID = r.Header.Get(common.ClientHeader) + ctx.ClientKey = r.Header.Get(common.ClientKeyHeader) + ctx.AllocationTx = vars["allocation"] + ctx.Signature = r.Header.Get(common.ClientSignatureHeader) + + if len(ctx.AllocationTx) > 0 { + alloc, err := allocation.GetOrCreate(ctx, ctx.Store, ctx.AllocationTx) + + if err != nil { + if errors.Is(common.ErrBadRequest, err) { + ctx.StatusCode = http.StatusBadRequest + + } else { + ctx.StatusCode = http.StatusInternalServerError + } + + return ctx, err + } + + ctx.Allocation = alloc + + valid, err := verifySignatureFromRequest(ctx.AllocationTx, ctx.Signature, alloc.OwnerPublicKey) + + if !valid { + ctx.StatusCode = http.StatusBadRequest + return ctx, errors.Throw(common.ErrBadRequest, "invalid signature "+ctx.Signature) + } + + if err != nil { + ctx.StatusCode = http.StatusInternalServerError + return ctx, errors.ThrowLog(err.Error(), common.ErrInternal, "invalid signature "+ctx.Signature) + } + } + + return ctx, nil +} diff --git a/code/go/0chain.net/blobbercore/handler/handler.go b/code/go/0chain.net/blobbercore/handler/handler.go index f10dbb5a2..bc2eba128 100644 --- a/code/go/0chain.net/blobbercore/handler/handler.go +++ b/code/go/0chain.net/blobbercore/handler/handler.go @@ -75,6 +75,11 @@ func SetupHandlers(r *mux.Router) { //marketplace related r.HandleFunc("/v1/marketplace/shareinfo/{allocation}", common.ToJSONResponse(WithConnection(MarketPlaceShareInfoHandler))) + + // lightweight http handler without heavy postgres transaction to improve performance + + r.HandleFunc("/v1/writemarker/lock/{allocation}", WithHandler(LockWriteMarker)).Methods(http.MethodPost) + r.HandleFunc("/v1/writemarker/lock/{allocation}", WithHandler(UnlockWriteMarker)).Methods(http.MethodDelete) } func WithReadOnlyConnection(handler common.JSONResponderF) common.JSONResponderF { diff --git a/code/go/0chain.net/blobbercore/handler/handler_writemarker.go b/code/go/0chain.net/blobbercore/handler/handler_writemarker.go new file mode 100644 index 000000000..02196fd5f --- /dev/null +++ b/code/go/0chain.net/blobbercore/handler/handler_writemarker.go @@ -0,0 +1,35 @@ +//go:build !integration_tests +// +build !integration_tests + +package handler + +import ( + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/writemarker" +) + +var WriteMarkerMutext = &writemarker.Mutex{} + +// LockWriteMarker try to lock writemarker for specified allocation id, and return latest RefTree +func LockWriteMarker(ctx *Context) (interface{}, error) { + sessionID := ctx.FormValue("session_id") + requestTime := ctx.FormTime("request_time") + + result, err := WriteMarkerMutext.Lock(ctx, ctx.AllocationTx, sessionID, requestTime) + if err != nil { + return nil, err + } + + return result, nil +} + +// UnlockWriteMarker release WriteMarkerMutex +func UnlockWriteMarker(ctx *Context) (interface{}, error) { + sessionID := ctx.FormValue("session_id") + + err := WriteMarkerMutext.Unlock(ctx, ctx.AllocationTx, sessionID) + if err != nil { + return nil, err + } + + return nil, nil +} diff --git a/code/go/0chain.net/blobbercore/handler/handler_writemarker_test.go b/code/go/0chain.net/blobbercore/handler/handler_writemarker_test.go new file mode 100644 index 000000000..48779982f --- /dev/null +++ b/code/go/0chain.net/blobbercore/handler/handler_writemarker_test.go @@ -0,0 +1,92 @@ +package handler + +import ( + "bytes" + "encoding/json" + "mime/multipart" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/writemarker" + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" +) + +func TestWriteMarkerHandlers_Lock(t *testing.T) { + + datastore.UseMocket(true) + + r := mux.NewRouter() + SetupHandlers(r) + + body := &bytes.Buffer{} + formWriter := multipart.NewWriter(body) + + now := time.Now() + + formWriter.WriteField("session_id", "session_id") //nolint: errcheck + formWriter.WriteField("request_time", strconv.FormatInt(now.Unix(), 10)) //nolint: errcheck + formWriter.Close() + + req, err := http.NewRequest(http.MethodPost, "/v1/writemarker/lock/{allocation}", body) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("Content-Type", formWriter.FormDataContentType()) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(WithHandler(func(ctx *Context) (interface{}, error) { + ctx.AllocationTx = "TestHandlers_Lock_allocation_id" + return LockWriteMarker(ctx) + })) + + handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + + var result writemarker.LockResult + + err = json.Unmarshal(rr.Body.Bytes(), &result) + require.Nil(t, err) + + require.Equal(t, writemarker.LockStatusOK, result.Status) +} + +func TestWriteMarkerHandlers_Unlock(t *testing.T) { + datastore.UseMocket(true) + + r := mux.NewRouter() + SetupHandlers(r) + + body := &bytes.Buffer{} + formWriter := multipart.NewWriter(body) + + now := time.Now() + + formWriter.WriteField("session_id", "session_id") //nolint: errcheck + formWriter.WriteField("request_time", strconv.FormatInt(now.Unix(), 10)) //nolint: errcheck + formWriter.Close() + + req, err := http.NewRequest(http.MethodDelete, "/v1/writemarker/lock/{allocation}", body) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("Content-Type", formWriter.FormDataContentType()) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(WithHandler(func(ctx *Context) (interface{}, error) { + ctx.AllocationTx = "TestHandlers_Unlock_allocation_id" + return UnlockWriteMarker(ctx) + })) + + handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + +} diff --git a/code/go/0chain.net/blobbercore/reference/dao.go b/code/go/0chain.net/blobbercore/reference/dao.go new file mode 100644 index 000000000..363ce4825 --- /dev/null +++ b/code/go/0chain.net/blobbercore/reference/dao.go @@ -0,0 +1,73 @@ +package reference + +import ( + "context" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + "github.com/0chain/blobber/code/go/0chain.net/core/common" + "github.com/0chain/errors" + "gorm.io/gorm" +) + +// LoadRootNode load root node with its descendant nodes +func LoadRootNode(ctx context.Context, allocationID string) (*HashNode, error) { + + db := datastore.GetStore().GetDB() + + db = db.Where("allocation_id = ? and deleted_at IS NULL", allocationID) + + db = db.Order("level desc, path") + + dict := make(map[string][]*HashNode) + + var nodes []*HashNode + // it is better to load them in batched if there are a lot of objects in db + err := db.FindInBatches(&nodes, 100, func(tx *gorm.DB, batch int) error { + // batch processing found records + for _, object := range nodes { + dict[object.ParentPath] = append(dict[object.ParentPath], object) + + for _, child := range dict[object.Path] { + object.AddChild(child) + } + } + + return nil + }).Error + + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + // create empty dir if root is missing + if len(dict) == 0 { + return &HashNode{AllocationID: allocationID, Type: DIRECTORY, Path: "/", Name: "/", ParentPath: ""}, nil + } + + rootNodes, ok := dict[""] + + if ok { + if len(rootNodes) == 1 { + return rootNodes[0], nil + } + + return nil, errors.Throw(common.ErrInternal, "invalid_ref_tree: / is missing or invalid") + } + + return nil, errors.Throw(common.ErrInternal, "invalid_ref_tree: / is missing or invalid") +} + +const ( + SQLWhereGetByAllocationTxAndPath = "reference_objects.allocation_id = ? and reference_objects.path = ? and deleted_at is NULL" +) + +// DryRun Creates a prepared statement when executing any SQL and caches them to speed up future calls +// https://gorm.io/docs/performance.html#Caches-Prepared-Statement +func DryRun(db *gorm.DB) { + + // https://gorm.io/docs/session.html#DryRun + // Session mode + //tx := db.Session(&gorm.Session{PrepareStmt: true, DryRun: true}) + + // use Table instead of Model to reduce reflect times +} diff --git a/code/go/0chain.net/blobbercore/reference/entity.go b/code/go/0chain.net/blobbercore/reference/entity.go new file mode 100644 index 000000000..2fae95c28 --- /dev/null +++ b/code/go/0chain.net/blobbercore/reference/entity.go @@ -0,0 +1,69 @@ +package reference + +import ( + "strconv" + "strings" + + "gorm.io/datatypes" +) + +// HashNode ref node in hash tree +type HashNode struct { + // hash data + AllocationID string `gorm:"column:allocation_id" json:"allocation_id,omitempty"` + Type string `gorm:"column:type" json:"type,omitempty"` + Name string `gorm:"column:name" json:"name,omitempty"` + Path string `gorm:"column:path" json:"path,omitempty"` + ContentHash string `gorm:"column:content_hash" json:"content_hash,omitempty"` + MerkleRoot string `gorm:"column:merkle_root" json:"merkle_root,omitempty"` + ActualFileHash string `gorm:"column:actual_file_hash" json:"actual_file_hash,omitempty"` + Attributes datatypes.JSON `gorm:"column:attributes" json:"attributes,omitempty"` + ChunkSize int64 `gorm:"column:chunk_size" json:"chunk_size,omitempty"` + Size int64 `gorm:"column:size" json:"size,omitempty"` + ActualFileSize int64 `gorm:"column:actual_file_size" json:"actual_file_size,omitempty"` + + // other data + ParentPath string `gorm:"parent_path" json:"-"` + Children []*HashNode `gorm:"-" json:"children,omitempty"` +} + +// TableName get table name of Ref +func (HashNode) TableName() string { + return TableNameReferenceObjects +} + +func (n *HashNode) AddChild(c *HashNode) { + if n.Children == nil { + n.Children = make([]*HashNode, 0, 10) + } + + n.Children = append(n.Children, c) +} + +// GetLookupHash get lookuphash +func (n *HashNode) GetLookupHash() string { + return GetReferenceLookup(n.AllocationID, n.Path) +} + +// GetHashCode get hash code +func (n *HashNode) GetHashCode() string { + + if len(n.Attributes) == 0 { + n.Attributes = datatypes.JSON("{}") + } + hashArray := []string{ + n.AllocationID, + n.Type, + n.Name, + n.Path, + strconv.FormatInt(n.Size, 10), + n.ContentHash, + n.MerkleRoot, + strconv.FormatInt(n.ActualFileSize, 10), + n.ActualFileHash, + string(n.Attributes), + strconv.FormatInt(n.ChunkSize, 10), + } + + return strings.Join(hashArray, ":") +} diff --git a/code/go/0chain.net/blobbercore/writemarker/mutex.go b/code/go/0chain.net/blobbercore/writemarker/mutex.go new file mode 100644 index 000000000..120163930 --- /dev/null +++ b/code/go/0chain.net/blobbercore/writemarker/mutex.go @@ -0,0 +1,164 @@ +package writemarker + +import ( + "context" + "sync" + "time" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/config" + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/reference" + "github.com/0chain/blobber/code/go/0chain.net/core/common" + "github.com/0chain/errors" + "github.com/0chain/gosdk/constants" + "gorm.io/gorm" +) + +// LockStatus lock status +type LockStatus int + +const ( + LockStatusFailed LockStatus = iota + LockStatusPending + LockStatusOK +) + +type LockResult struct { + Status LockStatus `json:"status,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + RootNode *reference.HashNode `json:"root_node,omitempty"` +} + +// Mutex WriteMarker mutex +type Mutex struct { + sync.Mutex +} + +// Lock +func (m *Mutex) Lock(ctx context.Context, allocationID, sessionID string, requestTime *time.Time) (*LockResult, error) { + m.Mutex.Lock() + defer m.Mutex.Unlock() + + if len(allocationID) == 0 { + return nil, errors.Throw(constants.ErrInvalidParameter, "allocationID") + } + + if len(sessionID) == 0 { + return nil, errors.Throw(constants.ErrInvalidParameter, "sessionID") + } + + if requestTime == nil { + return nil, errors.Throw(constants.ErrInvalidParameter, "requestTime") + } + + now := time.Now() + if requestTime.After(now.Add(config.Configuration.WriteMarkerLockTimeout)) { + return nil, errors.Throw(constants.ErrInvalidParameter, "requestTime") + } + + db := datastore.GetStore().GetDB() + + var lock datastore.WriteLock + err := db.Table(datastore.TableNameWriteLock).Where("allocation_id=?", allocationID).First(&lock).Error + if err != nil { + // new lock + if errors.Is(err, gorm.ErrRecordNotFound) { + lock = datastore.WriteLock{ + AllocationID: allocationID, + SessionID: sessionID, + CreatedAt: *requestTime, + } + + err = db.Create(&lock).Error + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + rootNode, err := reference.LoadRootNode(ctx, allocationID) + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + return &LockResult{ + Status: LockStatusOK, + CreatedAt: lock.CreatedAt, + RootNode: rootNode, + }, nil + + } + + //native postgres error + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + + } + + timeout := lock.CreatedAt.Add(config.Configuration.WriteMarkerLockTimeout) + + // locked, but it is timeout + if now.After(timeout) { + + lock.SessionID = sessionID + lock.CreatedAt = *requestTime + + err = db.Save(&lock).Error + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + rootNode, err := reference.LoadRootNode(ctx, allocationID) + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + return &LockResult{ + Status: LockStatusOK, + CreatedAt: lock.CreatedAt, + RootNode: rootNode, + }, nil + + } + + //try lock by same session, return old lock directly + if lock.SessionID == sessionID && lock.CreatedAt.Equal(*requestTime) { + rootNode, err := reference.LoadRootNode(ctx, allocationID) + if err != nil { + return nil, errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + return &LockResult{ + Status: LockStatusOK, + CreatedAt: lock.CreatedAt, + RootNode: rootNode, + }, nil + } + + // pending + return &LockResult{ + Status: LockStatusPending, + CreatedAt: lock.CreatedAt, + }, nil + +} + +func (*Mutex) Unlock(ctx context.Context, allocationID string, sessionID string) error { + + if len(allocationID) == 0 { + return nil + } + + if len(sessionID) == 0 { + return nil + } + + db := datastore.GetStore().GetDB() + + err := db.Where("allocation_id = ? and session_id =? ", allocationID, sessionID).Delete(&datastore.WriteLock{}).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.ThrowLog(err.Error(), common.ErrBadDataStore) + } + + return nil +} diff --git a/code/go/0chain.net/blobbercore/writemarker/mutext_test.go b/code/go/0chain.net/blobbercore/writemarker/mutext_test.go new file mode 100644 index 000000000..0c09c4f47 --- /dev/null +++ b/code/go/0chain.net/blobbercore/writemarker/mutext_test.go @@ -0,0 +1,199 @@ +package writemarker + +import ( + "context" + "testing" + "time" + + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/config" + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/datastore" + gomocket "github.com/selvatico/go-mocket" + "github.com/stretchr/testify/require" +) + +func TestMutext_LockShouldWork(t *testing.T) { + + datastore.UseMocket(false) + + config.Configuration.WriteMarkerLockTimeout = 30 * time.Second + + m := &Mutex{} + now := time.Now() + + tests := []struct { + name string + allocationID string + sessionID string + requestTime time.Time + mock func() + assert func(*testing.T, *LockResult, error) + }{ + { + name: "Lock should work", + allocationID: "lock_allocation_id", + sessionID: "lock_session_id", + requestTime: now, + mock: func() { + + }, + assert: func(test *testing.T, r *LockResult, err error) { + require.Nil(test, err) + require.Equal(test, LockStatusOK, r.Status) + }, + }, + { + name: "retry lock by same request should work if it is not timeout", + allocationID: "lock_same_allocation_id", + sessionID: "lock_same_session_id", + requestTime: now, + mock: func() { + gomocket.Catcher.NewMock(). + WithQuery(`SELECT * FROM "write_locks" WHERE allocation_id=$1 ORDER BY "write_locks"."allocation_id" LIMIT 1`). + WithArgs("lock_same_allocation_id"). + WithReply([]map[string]interface{}{ + { + "allocation_id": "lock_same_allocation_id", + "session_id": "lock_same_session_id", + "created_at": now, + }, + }) + }, + assert: func(test *testing.T, r *LockResult, err error) { + require.Nil(test, err) + require.Equal(test, LockStatusOK, r.Status) + }, + }, + { + name: "lock should be pending if it already is locked by other session ", + allocationID: "lock_allocation_id", + sessionID: "lock_pending_session_id", + requestTime: time.Now(), + mock: func() { + gomocket.Catcher.NewMock(). + WithQuery(`SELECT * FROM "write_locks" WHERE allocation_id=$1 ORDER BY "write_locks"."allocation_id" LIMIT 1`). + WithArgs("lock_allocation_id"). + WithReply([]map[string]interface{}{ + { + "allocation_id": "lock_allocation_id", + "session_id": "lock_session_id", + "created_at": time.Now().Add(-5 * time.Second), + }, + }) + }, + assert: func(test *testing.T, r *LockResult, err error) { + require.Nil(test, err) + require.Equal(test, LockStatusPending, r.Status) + }, + }, + { + name: "lock should ok if it is timeout", + allocationID: "lock_timeout_allocation_id", + sessionID: "lock_timeout_2nd_session_id", + requestTime: now, + mock: func() { + gomocket.Catcher.NewMock(). + WithQuery(`SELECT * FROM "write_locks" WHERE allocation_id=$1 ORDER BY "write_locks"."allocation_id" LIMIT 1`). + WithArgs("lock_timeout_allocation_id"). + WithReply([]map[string]interface{}{ + { + "allocation_id": "lock_timeout_allocation_id", + "session_id": "lock_timeout_1st_session_id", + "created_at": time.Now().Add(31 * time.Second), + }, + }) + }, + assert: func(test *testing.T, r *LockResult, err error) { + require.Nil(test, err) + require.Equal(test, LockStatusPending, r.Status) + }, + }, + } + + for _, it := range tests { + + t.Run(it.name, + func(test *testing.T) { + if it.mock != nil { + it.mock() + } + r, err := m.Lock(context.TODO(), it.allocationID, it.sessionID, &it.requestTime) + + it.assert(test, r, err) + + }, + ) + + } + +} + +func TestMutext_LockShouldNotWork(t *testing.T) { + + datastore.UseMocket(true) + + config.Configuration.WriteMarkerLockTimeout = 30 * time.Second + + m := &Mutex{} + now := time.Now() + + tests := []struct { + name string + allocationID string + sessionID string + requestTime time.Time + mock func() + assert func(*testing.T, *LockResult, error) + }{ + { + name: "Lock should not work if request_time is timeout", + allocationID: "lock_allocation_id", + sessionID: "lock_session_id", + requestTime: time.Now().Add(31 * time.Second), + mock: func() { + config.Configuration.WriteMarkerLockTimeout = 30 * time.Second + }, + assert: func(test *testing.T, r *LockResult, err error) { + require.Nil(test, r) + require.NotNil(test, err) + }, + }, + { + name: "retry lock by same request should not work if it is timeout", + allocationID: "lock_same_timeout_allocation_id", + sessionID: "lock_same_timeout_session_id", + requestTime: now, + mock: func() { + gomocket.Catcher.NewMock(). + WithQuery(`SELECT * FROM "write_locks" WHERE allocation_id=$1 ORDER BY "write_locks"."allocation_id" LIMIT 1`). + WithArgs("lock_same_timeout_allocation_id"). + WithReply([]map[string]interface{}{ + { + "allocation_id": "lock_same_timeout_allocation_id", + "session_id": "lock_same_timeout_session_id", + "created_at": now.Add(-config.Configuration.WriteMarkerLockTimeout), + }, + }) + }, + assert: func(test *testing.T, r *LockResult, err error) { + require.NotNil(test, err) + require.Nil(test, r) + }, + }, + } + + for _, it := range tests { + + t.Run(it.name, + func(test *testing.T) { + if it.mock != nil { + it.mock() + } + r, err := m.Lock(context.TODO(), it.allocationID, it.sessionID, &it.requestTime) + + it.assert(test, r, err) + + }, + ) + + } +} diff --git a/code/go/0chain.net/core/common/constants.go b/code/go/0chain.net/core/common/constants.go new file mode 100644 index 000000000..2345aebe1 --- /dev/null +++ b/code/go/0chain.net/core/common/constants.go @@ -0,0 +1,35 @@ +package common + +import "errors" + +var ( + // ErrBadDataStore bad db operation + ErrBadDataStore = errors.New("datastore: bad db operation") + + // ErrInvalidParameter parameter is not specified or invalid + ErrInvalidParameter = errors.New("invalid parameter") + + // ErrUnableHash failed to hash with unknown exception + ErrUnableHash = errors.New("unable to hash") + + // ErrUnableWriteFile failed to write bytes to file + ErrUnableWriteFile = errors.New("unable to write file") + + // ErrNotImplemented feature/method is not implemented yet + ErrNotImplemented = errors.New("not implemented") + + // ErrInvalidOperation failed to invoke a method + ErrInvalidOperation = errors.New("invalid operation") + + // ErrBadRequest bad request + ErrBadRequest = errors.New("bad request") + + // ErrUnknown unknown exception + ErrUnknown = errors.New("unknown") + + // ErrInternal an unknown internal server error + ErrInternal = errors.New("internal") + + // ErrEntityNotFound entity can't found in db + ErrEntityNotFound = errors.New("entity not found") +) diff --git a/go.mod b/go.mod index 58784a021..d9e844bef 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.16 require ( github.com/0chain/errors v1.0.3 - github.com/0chain/gosdk v1.5.1-0.20220211020847-2b54b2fa404f + github.com/0chain/gosdk v1.7.1-0.20220219170933-3eac488a6f15 github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/didip/tollbooth/v6 v6.1.1 + github.com/didip/tollbooth/v6 v6.1.2 github.com/go-ini/ini v1.55.0 // indirect github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 @@ -14,7 +14,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 github.com/herumi/bls-go-binary v1.0.1-0.20210830012634-a8e769d3b872 github.com/improbable-eng/grpc-web v0.15.0 - github.com/jackc/pgx/v4 v4.14.1 // indirect + github.com/jackc/pgx/v4 v4.15.0 // indirect github.com/koding/cache v0.0.0-20161222233015-e8a81b0b3f20 github.com/minio/minio-go v6.0.14+incompatible github.com/mitchellh/mapstructure v1.4.3 @@ -26,9 +26,10 @@ require ( github.com/stretchr/testify v1.7.0 go.uber.org/ratelimit v0.2.0 go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect google.golang.org/genproto v0.0.0-20220208230804-65c12eb4c068 google.golang.org/grpc v1.44.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 @@ -36,8 +37,8 @@ require ( gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gorm.io/datatypes v0.0.0-20200806042100-bc394008dd0d - gorm.io/driver/postgres v1.2.3 - gorm.io/gorm v1.22.5 + gorm.io/driver/postgres v1.3.1 + gorm.io/gorm v1.23.1 nhooyr.io/websocket v1.8.7 // indirect ) diff --git a/go.sum b/go.sum index ace573d4c..a154c6548 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/0chain/errors v1.0.3 h1:QQZPFxTfnMcRdt32DXbzRQIfGWmBsKoEdszKQDb0rRM= github.com/0chain/errors v1.0.3/go.mod h1:xymD6nVgrbgttWwkpSCfLLEJbFO6iHGQwk/yeSuYkIc= -github.com/0chain/gosdk v1.5.1-0.20220211020847-2b54b2fa404f h1:dLjkhdiMIY9Rs3mUlEAPqAQKf+lE1NLKFd+bzVkQyzk= -github.com/0chain/gosdk v1.5.1-0.20220211020847-2b54b2fa404f/go.mod h1:G/JUrqvT2WStxFbSpJKnU1Wt37GyatimoqPJfEE10bs= +github.com/0chain/gosdk v1.7.1-0.20220219170933-3eac488a6f15 h1:cOqg66kR646dc1NNgduE0Ridlg0bDDvhxOPC9rhrNWk= +github.com/0chain/gosdk v1.7.1-0.20220219170933-3eac488a6f15/go.mod h1:G/JUrqvT2WStxFbSpJKnU1Wt37GyatimoqPJfEE10bs= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4= @@ -226,8 +226,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= -github.com/didip/tollbooth/v6 v6.1.1 h1:Nt7PvWLa9Y94OrykXsFNBinVRQIu8xdy4avpl99Dc1M= -github.com/didip/tollbooth/v6 v6.1.1/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s= +github.com/didip/tollbooth/v6 v6.1.2 h1:Kdqxmqw9YTv0uKajBUiWQg+GURL/k4vy9gmLCL01PjQ= +github.com/didip/tollbooth/v6 v6.1.2/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -555,8 +555,9 @@ github.com/jackc/pgconn v1.6.1/go.mod h1:g8mKMqmSUO6AzAvha7vy07g1rbGOlc7iF0nU0ei github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= +github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -588,9 +589,9 @@ github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkAL github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.4.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0= github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= +github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= @@ -599,15 +600,16 @@ github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6 github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.7.1/go.mod h1:nu42q3aPjuC1M0Nak4bnoprKlXPINqopEKqbq5AZSC4= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= -github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU= github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M= +github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= +github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= @@ -616,7 +618,6 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -1082,8 +1083,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1331,8 +1332,9 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1627,8 +1629,8 @@ gorm.io/datatypes v0.0.0-20200806042100-bc394008dd0d/go.mod h1:n2DTgk9at7cr/CWOT gorm.io/driver/mysql v0.3.1 h1:yvUT7Q0I3B9EHJ67NSp6cHbVwcdDHhVUsDAUiFFxRk0= gorm.io/driver/mysql v0.3.1/go.mod h1:A7H1JD9dKdcjeUTpTuWKEC+E1a74qzW7/zaXqKaTbfM= gorm.io/driver/postgres v0.2.6/go.mod h1:AsPyuhKFOplSmQwOPsycVKbe0dRxF8v18KZ7p9i8dIs= -gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To= -gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs= +gorm.io/driver/postgres v1.3.1 h1:Pyv+gg1Gq1IgsLYytj/S2k7ebII3CzEdpqQkPOdH24g= +gorm.io/driver/postgres v1.3.1/go.mod h1:WwvWOuR9unCLpGWCL6Y3JOeBWvbKi6JLhayiVclSZZU= gorm.io/driver/sqlite v1.0.8 h1:omllgSb7/eh9D6lGvLZOdU1ZElxdXuO3dn3Rk+dQxUE= gorm.io/driver/sqlite v1.0.8/go.mod h1:xkm8/CEmA3yc4zRd0pdCqm43BjO8Hm6avfTpxWb/7c4= gorm.io/driver/sqlserver v0.2.5 h1:o/MXpn9/BB68RXEEQzfhsSL382yEqUtdCiGIuCspmkY= @@ -1636,9 +1638,8 @@ gorm.io/driver/sqlserver v0.2.5/go.mod h1:TcPfkdce5b8qlCMgyUeUdm7HQa1ZzWUuxzI+od gorm.io/gorm v0.2.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v0.2.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v0.2.27/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.22.5 h1:lYREBgc02Be/5lSCTuysZZDb6ffL2qrat6fg9CFbvXU= -gorm.io/gorm v1.22.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.1 h1:aj5IlhDzEPsoIyOPtTRVI+SyaN1u6k613sbt4pwbxG0= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=