Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(database/gdb): deadlock when orm operations performing in cache closure function from gcache #3585

Merged
merged 10 commits into from
May 22, 2024
Merged
17 changes: 8 additions & 9 deletions database/gdb/gdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ type Core struct {
logger glog.ILogger // Logger for logging functionality.
config *ConfigNode // Current config node.
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
innerMemCache *gcache.Cache
}

type dynamicConfig struct {
Expand Down Expand Up @@ -525,9 +526,6 @@ var (
// allDryRun sets dry-run feature for all database connections.
// It is commonly used for command options for convenience.
allDryRun = false

// tableFieldsMap caches the table information retrieved from database.
tableFieldsMap = gmap.NewStrAnyMap(true)
)

func init() {
Expand Down Expand Up @@ -587,12 +585,13 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) {
node = parseConfigNodeLink(node)
}
c := &Core{
group: group,
debug: gtype.NewBool(),
cache: gcache.New(),
links: gmap.New(true),
logger: glog.New(),
config: node,
group: group,
debug: gtype.NewBool(),
cache: gcache.New(),
links: gmap.New(true),
logger: glog.New(),
config: node,
innerMemCache: gcache.New(),
dynamicConfig: dynamicConfig{
MaxIdleConnCount: node.MaxIdleConnCount,
MaxOpenConnCount: node.MaxOpenConnCount,
Expand Down
20 changes: 14 additions & 6 deletions database/gdb/gdb_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
Expand Down Expand Up @@ -737,20 +738,27 @@ func (c *Core) HasTable(name string) (bool, error) {
return false, nil
}

func (c *Core) GetInnerMemCache() *gcache.Cache {
return c.innerMemCache
}

// GetTablesWithCache retrieves and returns the table names of current database with cache.
func (c *Core) GetTablesWithCache() ([]string, error) {
var (
ctx = c.db.GetCtx()
cacheKey = fmt.Sprintf(`Tables: %s`, c.db.GetGroup())
ctx = c.db.GetCtx()
cacheKey = fmt.Sprintf(`Tables: %s`, c.db.GetGroup())
cacheDuration = gcache.DurationNoExpire
innerMemCache = c.GetInnerMemCache()
)
result, err := c.GetCache().GetOrSetFuncLock(
ctx, cacheKey, func(ctx context.Context) (interface{}, error) {
result, err := innerMemCache.GetOrSetFuncLock(
ctx, cacheKey,
func(ctx context.Context) (interface{}, error) {
tableList, err := c.db.Tables(ctx)
if err != nil {
return false, err
return nil, err
}
return tableList, nil
}, 0,
}, cacheDuration,
)
if err != nil {
return nil, err
Expand Down
32 changes: 8 additions & 24 deletions database/gdb/gdb_core_utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ package gdb

import (
"context"
"fmt"

"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gutil"
)

Expand Down Expand Up @@ -144,30 +141,30 @@ func (c *Core) TableFields(ctx context.Context, table string, schema ...string)

// ClearTableFields removes certain cached table fields of current configuration group.
func (c *Core) ClearTableFields(ctx context.Context, table string, schema ...string) (err error) {
tableFieldsMap.Remove(fmt.Sprintf(
`%s%s@%s#%s`,
cachePrefixTableFields,
tableFieldsCacheKey := genTableFieldsCacheKey(
c.db.GetGroup(),
gutil.GetOrDefaultStr(c.db.GetSchema(), schema...),
table,
))
)
_, err = c.innerMemCache.Remove(ctx, tableFieldsCacheKey)
return
}

// ClearTableFieldsAll removes all cached table fields of current configuration group.
func (c *Core) ClearTableFieldsAll(ctx context.Context) (err error) {
var (
keys = tableFieldsMap.Keys()
cachePrefix = fmt.Sprintf(`%s%s`, cachePrefixTableFields, c.db.GetGroup())
removedKeys = make([]string, 0)
keys, _ = c.innerMemCache.KeyStrings(ctx)
cachePrefix = cachePrefixTableFields
removedKeys = make([]any, 0)
)
for _, key := range keys {
if gstr.HasPrefix(key, cachePrefix) {
removedKeys = append(removedKeys, key)
}
}

if len(removedKeys) > 0 {
tableFieldsMap.Removes(removedKeys)
err = c.innerMemCache.Removes(ctx, removedKeys)
}
return
}
Expand All @@ -182,19 +179,6 @@ func (c *Core) ClearCacheAll(ctx context.Context) (err error) {
return c.db.GetCache().Clear(ctx)
}

func (c *Core) makeSelectCacheKey(name, schema, table, sql string, args ...interface{}) string {
if name == "" {
name = fmt.Sprintf(
`%s@%s#%s:%s`,
c.db.GetGroup(),
schema,
table,
gmd5.MustEncryptString(sql+", @PARAMS:"+gconv.String(args)),
)
}
return fmt.Sprintf(`%s%s`, cachePrefixSelectCache, name)
}

// HasField determine whether the field exists in the table.
func (c *Core) HasField(ctx context.Context, table, field string, schema ...string) (bool, error) {
table = c.guessPrimaryTableName(table)
Expand Down
24 changes: 13 additions & 11 deletions database/gdb/gdb_driver_wrapper_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gutil"
)
Expand Down Expand Up @@ -68,24 +69,25 @@
)
}
var (
innerMemCache = d.GetCore().GetInnerMemCache()
// prefix:group@schema#table
cacheKey = fmt.Sprintf(
`%s%s@%s#%s`,
cachePrefixTableFields,
cacheKey = genTableFieldsCacheKey(
d.GetGroup(),
gutil.GetOrDefaultStr(d.GetSchema(), schema...),
table,
)
value = tableFieldsMap.GetOrSetFuncLock(cacheKey, func() interface{} {
ctx = context.WithValue(ctx, ctxKeyInternalProducedSQL, struct{}{})
fields, err = d.DB.TableFields(ctx, table, schema...)
if err != nil {
return nil
}
return fields
})
cacheFunc = func(ctx context.Context) (interface{}, error) {
return d.DB.TableFields(
context.WithValue(ctx, ctxKeyInternalProducedSQL, struct{}{}),
table, schema...,
)
}
value any
)
value, err = innerMemCache.GetOrSetFuncLock(

Check failure on line 87 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.20)

SA4023(related information): the lhs of the comparison gets its value from here and has a concrete type (staticcheck)

Check failure on line 87 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.21.4)

SA4023(related information): the lhs of the comparison gets its value from here and has a concrete type (staticcheck)

Check failure on line 87 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.18)

SA4023(related information): the lhs of the comparison gets its value from here and has a concrete type (staticcheck)

Check failure on line 87 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.20)

SA4023(related information): the lhs of the comparison gets its value from here and has a concrete type (staticcheck)
ctx, cacheKey, cacheFunc, gcache.DurationNoExpire,
)
gqcn marked this conversation as resolved.
Show resolved Hide resolved
if value != nil {

Check failure on line 90 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.20)

SA4023: this comparison is always true (staticcheck)

Check failure on line 90 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.21.4)

SA4023: this comparison is always true (staticcheck)

Check failure on line 90 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.18)

SA4023: this comparison is always true (staticcheck)

Check failure on line 90 in database/gdb/gdb_driver_wrapper_db.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.20)

SA4023: this comparison is always true (staticcheck)
fields = value.(map[string]*TableField)
}
return
Expand Down
24 changes: 24 additions & 0 deletions database/gdb/gdb_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"time"

"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/encoding/ghash"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/intlog"
Expand Down Expand Up @@ -944,3 +945,26 @@ func FormatMultiLineSqlToSingle(sql string) (string, error) {
}
return sql, nil
}

func genTableFieldsCacheKey(group, schema, table string) string {
return fmt.Sprintf(
`%s%s@%s#%s`,
cachePrefixTableFields,
group,
schema,
table,
)
}

func genSelectCacheKey(name, group, schema, table, sql string, args ...interface{}) string {
if name == "" {
name = fmt.Sprintf(
`%s@%s#%s:%d`,
group,
schema,
table,
ghash.BKDR64([]byte(sql+", @PARAMS:"+gconv.String(args))),
)
}
return fmt.Sprintf(`%s%s`, cachePrefixSelectCache, name)
}
3 changes: 2 additions & 1 deletion database/gdb/gdb_model_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ func (m *Model) saveSelectResultToCache(
}

func (m *Model) makeSelectCacheKey(sql string, args ...interface{}) string {
return m.db.GetCore().makeSelectCacheKey(
return genSelectCacheKey(
m.cacheOption.Name,
m.db.GetGroup(),
m.db.GetSchema(),
m.db.GetCore().guessPrimaryTableName(m.tables),
sql,
Expand Down
54 changes: 31 additions & 23 deletions database/gdb/gdb_model_soft_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,43 +190,51 @@ func (m *softTimeMaintainer) getSoftFieldNameAndType(
schema string, table string, checkFiledNames []string,
) (fieldName string, fieldType LocalType) {
var (
cacheKey = fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%s`, schema, table, strings.Join(checkFiledNames, "_"))
innerMemCache = m.db.GetCore().GetInnerMemCache()
cacheKey = fmt.Sprintf(
`getSoftFieldNameAndType:%s#%s#%s`,
schema, table, strings.Join(checkFiledNames, "_"),
)
cacheDuration = gcache.DurationNoExpire
cacheFunc = func(ctx context.Context) (value interface{}, err error) {
// Ignore the error from TableFields.
fieldsMap, _ := m.TableFields(table, schema)
if len(fieldsMap) > 0 {
for _, checkFiledName := range checkFiledNames {
fieldName, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), checkFiledName,
fieldsMap, err := m.TableFields(table, schema)
if err != nil {
return nil, err
}
if len(fieldsMap) == 0 {
return nil, nil
}
for _, checkFiledName := range checkFiledNames {
fieldName, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), checkFiledName,
)
if fieldName != "" {
fieldType, _ = m.db.CheckLocalTypeForField(
ctx, fieldsMap[fieldName].Type, nil,
)
if fieldName != "" {
fieldType, _ = m.db.CheckLocalTypeForField(
ctx, fieldsMap[fieldName].Type, nil,
)
var cacheItem = getSoftFieldNameAndTypeCacheItem{
FieldName: fieldName,
FieldType: fieldType,
}
return cacheItem, nil
var cacheItem = getSoftFieldNameAndTypeCacheItem{
FieldName: fieldName,
FieldType: fieldType,
}
return cacheItem, nil
}
}
return
}
)
result, err := gcache.GetOrSetFunc(ctx, cacheKey, cacheFunc, cacheDuration)
result, err := innerMemCache.GetOrSetFunc(
ctx, cacheKey, cacheFunc, cacheDuration,
)
if err != nil {
intlog.Error(ctx, err)
return
}
if result != nil {
var cacheItem getSoftFieldNameAndTypeCacheItem
if err = result.Scan(&cacheItem); err != nil {
return "", ""
}
fieldName = cacheItem.FieldName
fieldType = cacheItem.FieldType
return
gqcn marked this conversation as resolved.
Show resolved Hide resolved
}
cacheItem := result.Val().(getSoftFieldNameAndTypeCacheItem)
fieldName = cacheItem.FieldName
fieldType = cacheItem.FieldType
return
}

Expand Down
Loading