From 42ca0a670734a88f86bc1d0aba8ff4985e395304 Mon Sep 17 00:00:00 2001 From: a-barboza <29963827+a-barboza@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:27:18 -0700 Subject: [PATCH] DB Access Layer Merges: GetTablePattern ... (#103) Added the following APIs: ExistKeysPattern() GetTablePattern() Key.IsAllKeyPattern() Go UT, and Benchmark Tests for the same TranslibDBScriptFail error type to translib/tlerr package --- translib/db/db_key.go | 9 + translib/db/db_keys_pattern.go | 200 ++++++++++++++++++ translib/db/db_keys_pattern_test.go | 128 ++++++++++++ translib/db/db_table_pattern.go | 296 +++++++++++++++++++++++++++ translib/db/db_table_pattern_test.go | 223 ++++++++++++++++++++ translib/tlerr/tlerr.go | 8 + 6 files changed, 864 insertions(+) create mode 100644 translib/db/db_keys_pattern.go create mode 100644 translib/db/db_keys_pattern_test.go create mode 100644 translib/db/db_table_pattern.go create mode 100644 translib/db/db_table_pattern_test.go diff --git a/translib/db/db_key.go b/translib/db/db_key.go index 8e5c39386cb3..a0274a8a111c 100644 --- a/translib/db/db_key.go +++ b/translib/db/db_key.go @@ -101,6 +101,15 @@ func (k Key) Matches(pattern Key) bool { return true } +// IsAllKeyPattern returns true if it is an all key wildcard pattern. +// (i.e. A key with a single component "*") +func (k *Key) IsAllKeyPattern() bool { + if (len(k.Comp) == 1) && (k.Comp[0] == "*") { + return true + } + return false +} + // patternMatch checks if the value matches a key pattern. // vIndex and pIndex are start positions of value and pattern strings to match. // Mimics redis pattern matcher - i.e, glob like pattern matcher which diff --git a/translib/db/db_keys_pattern.go b/translib/db/db_keys_pattern.go new file mode 100644 index 000000000000..c1bc565f5c9f --- /dev/null +++ b/translib/db/db_keys_pattern.go @@ -0,0 +1,200 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// Copyright 2022 Broadcom. The term Broadcom refers to Broadcom Inc. and/or // +// its subsidiaries. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); // +// you may not use this file except in compliance with the License. // +// You may obtain a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +// // +//////////////////////////////////////////////////////////////////////////////// + +package db + +import ( + "time" + + "github.com/Azure/sonic-mgmt-common/translib/tlerr" + "github.com/go-redis/redis/v7" + "github.com/golang/glog" +) + +//////////////////////////////////////////////////////////////////////////////// +// Exported Types // +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Exported Functions // +//////////////////////////////////////////////////////////////////////////////// + +// ExistKeysPattern checks if a key pattern exists in a table +// Note: +// 1. Statistics do not capture when the CAS Tx Cache results in a quicker +// response. This is to avoid mis-interpreting per-connection statistics. +func (d *DB) ExistKeysPattern(ts *TableSpec, pat Key) (bool, error) { + + var err error + var exists bool + + // ExistsKeysPatternHits + // Time Start + var cacheHit bool + var now time.Time + var dur time.Duration + var stats Stats + + if (d == nil) || (d.client == nil) { + return exists, tlerr.TranslibDBConnectionReset{} + } + + if d.dbStatsConfig.TimeStats { + now = time.Now() + } + + if glog.V(3) { + glog.Info("ExistKeysPattern: Begin: ", "ts: ", ts, "pat: ", pat) + } + + defer func() { + if err != nil { + glog.Error("ExistKeysPattern: ts: ", ts, " err: ", err) + } + if glog.V(3) { + glog.Info("ExistKeysPattern: End: ts: ", ts, " exists: ", exists) + } + }() + + // If pseudoDB then follow the path of !IsWriteDisabled with Tx Cache. TBD. + + if !d.Opts.IsWriteDisabled { + + // If Write is enabled, then just call GetKeysPattern() and check + // for now. + + // Optimization: Check the DBL CAS Tx Cache for added entries. + for k := range d.txTsEntryMap[ts.Name] { + key := d.redis2key(ts, k) + if key.Matches(pat) { + if len(d.txTsEntryMap[ts.Name][k].Field) > 0 { + exists = true + break + } + // Removed entries/fields, can't help much, since we'd have + // to compare against the DB retrieved keys anyway + } + } + + if !exists { + var getKeys []Key + if getKeys, err = d.GetKeysPattern(ts, pat); (err == nil) && len(getKeys) > 0 { + + exists = true + } + } + + } else if d.dbCacheConfig.PerConnection && + d.dbCacheConfig.isCacheTable(ts.Name) { + + // Check PerConnection cache first, [Found = SUCCESS return] + var keys []Key + if table, ok := d.cache.Tables[ts.Name]; ok { + if keys, ok = table.patterns[d.key2redis(ts, pat)]; ok && len(keys) > 0 { + + exists = true + cacheHit = true + + } + } + } + + // Run Lua script [Found = SUCCESS return] + if d.Opts.IsWriteDisabled && !exists { + + //glog.Info("ExistKeysPattern: B4= ", luaScriptExistsKeysPatterns.Hash()) + + var luaExists interface{} + if luaExists, err = luaScriptExistsKeysPatterns.Run(d.client, + []string{d.key2redis(ts, pat)}).Result(); err == nil { + + if existsString, ok := luaExists.(string); !ok { + err = tlerr.TranslibDBScriptFail{ + Description: "Unexpected response"} + } else if existsString == "true" { + exists = true + } else if existsString != "false" { + err = tlerr.TranslibDBScriptFail{Description: existsString} + } + } + + //glog.Info("ExistKeysPattern: AF= ", luaScriptExistsKeysPatterns.Hash()) + } + + // Time End, Time, Peak + if d.dbStatsConfig.TableStats { + stats = d.stats.Tables[ts.Name] + } else { + stats = d.stats.AllTables + } + + stats.Hits++ + stats.ExistsKeyPatternHits++ + if cacheHit { + stats.ExistsKeyPatternCacheHits++ + } + + if d.dbStatsConfig.TimeStats { + dur = time.Since(now) + + if dur > stats.Peak { + stats.Peak = dur + } + stats.Time += dur + + if dur > stats.ExistsKeyPatternPeak { + stats.ExistsKeyPatternPeak = dur + } + stats.ExistsKeyPatternTime += dur + } + + if d.dbStatsConfig.TableStats { + d.stats.Tables[ts.Name] = stats + } else { + d.stats.AllTables = stats + } + + return exists, err +} + +//////////////////////////////////////////////////////////////////////////////// +// Internal Functions // +//////////////////////////////////////////////////////////////////////////////// + +var luaScriptExistsKeysPatterns *redis.Script + +func init() { + // Register the Lua Script + luaScriptExistsKeysPatterns = redis.NewScript(` + for i,k in pairs(redis.call('KEYS', KEYS[1])) do + return 'true' + end + return 'false' + `) + + // Alternate Lua Script + // luaScriptExistsKeysPatterns = redis.NewScript(` + // if #redis.call('KEYS', KEYS[1]) > 0 then + // return 'true' + // else + // return 'false' + // end + // `) + +} diff --git a/translib/db/db_keys_pattern_test.go b/translib/db/db_keys_pattern_test.go new file mode 100644 index 000000000000..e730de23a27c --- /dev/null +++ b/translib/db/db_keys_pattern_test.go @@ -0,0 +1,128 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// Copyright 2022 Broadcom. The term Broadcom refers to Broadcom Inc. and/or // +// its subsidiaries. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); // +// you may not use this file except in compliance with the License. // +// You may obtain a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +// // +//////////////////////////////////////////////////////////////////////////////// + +package db + +import ( + "testing" +) + +func BenchmarkExistKeysPattern(b *testing.B) { + for i := 0; i < b.N; i++ { + if exists, e := db.ExistKeysPattern(&ts, Key{Comp: []string{"*"}}); e != nil || !exists { + b.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } + } +} + +func BenchmarkGetKeysPattern(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, e := db.GetKeysPattern(&ts, Key{Comp: []string{"*"}}); e != nil { + b.Errorf("GetKeysPattern() returns err: %v", e) + } + } +} + +func TestGetKeysPattern(t *testing.T) { + if keys, e := db.GetKeysPattern(&ts, Key{Comp: []string{"*"}}); e != nil || len(keys) == 0 { + t.Errorf("GetKeysPattern() returns len(keys) == 0 || err: %v", e) + } +} + +func TestExistKeysPattern(t *testing.T) { + if exists, e := db.ExistKeysPattern(&ts, Key{Comp: []string{"*"}}); e != nil || !exists { + t.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } +} + +func TestExistKeysPatternSinglular(t *testing.T) { + if exists, e := db.ExistKeysPattern(&ts, Key{Comp: []string{"KEY1"}}); e != nil || !exists { + t.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } +} + +func TestExistKeysPatternGeneric(t *testing.T) { + if exists, e := db.ExistKeysPattern(&ts, Key{Comp: []string{"KEY*"}}); e != nil || !exists { + t.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } +} + +func TestExistKeysPatternEmpty(t *testing.T) { + if exists, e := db.ExistKeysPattern(&TableSpec{Name: "UNLIKELY_23"}, + Key{Comp: []string{"*"}}); e != nil || exists { + t.Errorf("ExistKeysPattern() returns exists || err: %v", e) + } + + if exists, e := db.ExistKeysPattern(&ts, Key{Comp: []string{"UNKNOWN"}}); e != nil || exists { + t.Errorf("ExistKeysPattern() returns exists || err: %v", e) + } +} + +func TestExistKeysPatternRW(t *testing.T) { + dbRW, err := newDB(ConfigDB) + if err != nil { + t.Fatalf("TestExistKeysPatternRW: newDB() for RW fails err = %v\n", err) + } + t.Cleanup(func() { dbRW.DeleteDB() }) + + if exists, e := dbRW.ExistKeysPattern(&ts, Key{Comp: []string{"*"}}); e != nil || !exists { + t.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } +} + +func TestExistKeysPatternSinglularRW(t *testing.T) { + dbRW, err := newDB(ConfigDB) + if err != nil { + t.Fatalf("TestExistKeysPatternSinglularRW: newDB() for RW fails err = %v\n", err) + } + t.Cleanup(func() { dbRW.DeleteDB() }) + + if exists, e := dbRW.ExistKeysPattern(&ts, Key{Comp: []string{"KEY1"}}); e != nil || !exists { + t.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } +} + +func TestExistKeysPatternGenericRW(t *testing.T) { + dbRW, err := newDB(ConfigDB) + if err != nil { + t.Fatalf("TestExistKeysPatternGenericRW: newDB() for RW fails err = %v\n", err) + } + t.Cleanup(func() { dbRW.DeleteDB() }) + + if exists, e := dbRW.ExistKeysPattern(&ts, Key{Comp: []string{"KEY*"}}); e != nil || !exists { + t.Errorf("ExistKeysPattern() returns !exists || err: %v", e) + } +} + +func TestExistKeysPatternEmptyRW(t *testing.T) { + dbRW, err := newDB(ConfigDB) + if err != nil { + t.Fatalf("TestExistKeysPatternEmptyRW: newDB() for RW fails err = %v\n", err) + } + t.Cleanup(func() { dbRW.DeleteDB() }) + + if exists, e := dbRW.ExistKeysPattern(&TableSpec{Name: "UNLIKELY_23"}, + Key{Comp: []string{"*"}}); e != nil || exists { + t.Errorf("ExistKeysPattern() returns exists || err: %v", e) + } + + if exists, e := dbRW.ExistKeysPattern(&ts, Key{Comp: []string{"UNKNOWN"}}); e != nil || exists { + t.Errorf("ExistKeysPattern() returns exists || err: %v", e) + } +} diff --git a/translib/db/db_table_pattern.go b/translib/db/db_table_pattern.go new file mode 100644 index 000000000000..a666ce55e30f --- /dev/null +++ b/translib/db/db_table_pattern.go @@ -0,0 +1,296 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// Copyright 2022 Broadcom. The term Broadcom refers to Broadcom Inc. and/or // +// its subsidiaries. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); // +// you may not use this file except in compliance with the License. // +// You may obtain a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +// // +//////////////////////////////////////////////////////////////////////////////// + +package db + +import ( + "time" + + "github.com/Azure/sonic-mgmt-common/translib/tlerr" + "github.com/go-redis/redis/v7" + "github.com/golang/glog" +) + +//////////////////////////////////////////////////////////////////////////////// +// Exported Types // +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Exported Functions // +//////////////////////////////////////////////////////////////////////////////// + +// GetTablePattern is similar to GetTable, except, it gets a subset of +// the table restricted by the key pattern "pat". It is expected that there +// will be a single call to the underlying go-redis layer, perhaps through +// the use of a script. However, the caller is not advised to rely on this, +// and encouraged to use this instead of the current GetTable() for reasonably +// sized subsets of tables ("reasonably sized" to be determined by experiments) +// to obtain a more performant behavior. +// Note: The time/hit statistics do not cover the error cases. +func (d *DB) GetTablePattern(ts *TableSpec, pat Key) (Table, error) { + + var err error + var ok bool + var keys []Key + var redisKey string + var redisValue []interface{} + var tkNv []interface{} + var luaTable interface{} + + // GetTablePatternHits + // Time Start + var cacheHit bool + var now time.Time + var dur time.Duration + var stats Stats + + if (d == nil) || (d.client == nil) { + return Table{}, tlerr.TranslibDBConnectionReset{} + } + + if d.dbStatsConfig.TimeStats { + now = time.Now() + } + + if glog.V(3) { + glog.Info("GetTablePattern: Begin: ts: ", ts, " pat: ", pat) + } + + defer func() { + if err != nil { + glog.Error("GetTablePattern: ts: ", ts, " err: ", err) + } + if glog.V(3) { + glog.Info("GetTablePattern: End: ts: ", ts) + } + }() + + // If pseudoDB then do custom. TBD. + + isComplete := false + if pat.IsAllKeyPattern() { + isComplete = true + } + // Create Table + table := Table{ + ts: ts, + entry: make(map[string]Value, InitialTableEntryCount), + complete: isComplete, + patterns: make(map[string][]Key, InitialTablePatternCount), + db: d, + } + + // Check Per Connection Cache first. + if (d.dbCacheConfig.PerConnection && + d.dbCacheConfig.isCacheTable(ts.Name)) || + (d.Opts.IsOnChangeEnabled && d.onCReg.isCacheTable(ts.Name)) { + + if cTable, ok := d.cache.Tables[ts.Name]; ok && cTable.complete { + + // Copy relevent keys of cTable to table, and set keys + // Be aware of cache poisoning + for redisKey, value := range cTable.entry { + key := d.redis2key(ts, redisKey) + // {Comp: []string{"*"}} matches {Comp: []string{"1", "2"}} + // However, Key.Matches() goes strictly by len(Comp) + if isComplete || key.Matches(pat) { + table.entry[redisKey] = value.Copy() + keys = append(keys, key) + } + } + table.patterns[d.key2redis(ts, pat)] = keys + cacheHit = true + + goto GetTablePatternFoundCache + } + } + + // Run the Lua script + luaTable, err = luaScriptGetTable.Run(d.client, + []string{d.key2redis(ts, pat)}).Result() + if err != nil { + return table, err + } + + // Walk through the results + // Initialize table.patterns + // Set table.entry entry + + tkNv, ok = luaTable.([]interface{}) + + if !ok { + err = tlerr.TranslibDBScriptFail{Description: "Unexpected list"} + return table, err + } + + keys = make([]Key, 0, len(tkNv)/2) + for i, v := range tkNv { + // glog.Info("GetTablePattern: i: ", i, " v: ", v) + if i%2 == 0 { + if redisKey, ok = v.(string); !ok { + err = tlerr.TranslibDBScriptFail{Description: "Unexpected key"} + return table, err + } + } else { + // glog.Info("GetTablePattern: i: ", i, " v: ", v) + if redisValue, ok = v.([]interface{}); !ok { + err = tlerr.TranslibDBScriptFail{Description: "Unexpected hash"} + return table, err + } + value := Value{Field: make(map[string]string, len(redisValue)/2)} + var fstr, fn string + for j, f := range redisValue { + if fstr, ok = f.(string); !ok { + err = tlerr.TranslibDBScriptFail{Description: "Unexpected field"} + return table, err + } + if j%2 == 0 { + fn = fstr + } else { + value.Field[fn] = fstr + } + } + table.entry[redisKey] = value + keys = append(keys, d.redis2key(ts, redisKey)) + } + } + +GetTablePatternFoundCache: + + // Populate the PerConnection cache, if enabled, allKeyPat, and cacheMiss + if !cacheHit && isComplete && + ((d.dbCacheConfig.PerConnection && + d.dbCacheConfig.isCacheTable(ts.Name)) || + (d.Opts.IsOnChangeEnabled && d.onCReg.isCacheTable(ts.Name))) { + + if _, ok := d.cache.Tables[ts.Name]; !ok { + d.cache.Tables[ts.Name] = Table{ + ts: ts, + entry: make(map[string]Value, InitialTableEntryCount), + complete: table.complete, + patterns: make(map[string][]Key, InitialTablePatternCount), + db: d, + } + } + + // Note: keys (and thus keysCopy stored in the PerConnection cache) + // should be *before* adjusting with Redis CAS Tx Cache + keysCopy := make([]Key, len(keys)) + for i, key := range keys { + keysCopy[i] = key.Copy() + } + d.cache.Tables[ts.Name].patterns[d.key2redis(ts, pat)] = keysCopy + + for redisKey, value := range table.entry { + d.cache.Tables[ts.Name].entry[redisKey] = value.Copy() + } + } + + // Reconcile with Redis CAS Transaction cache + for k := range d.txTsEntryMap[ts.Name] { + var present bool + var index int + key := d.redis2key(ts, k) + + if !isComplete && !key.Matches(pat) { + continue + } + + for i := 0; i < len(keys); i++ { + if key.Equals(keys[i]) { + index = i + present = true + break + } + } + + if !present { + if len(d.txTsEntryMap[ts.Name][k].Field) > 0 { + keys = append(keys, key) + table.entry[k] = (d.txTsEntryMap[ts.Name][k]).Copy() + } + } else { + if len(d.txTsEntryMap[ts.Name][k].Field) == 0 { + keys = append(keys[:index], keys[index+1:]...) + delete(table.entry, k) + } else { + table.entry[k] = (d.txTsEntryMap[ts.Name][k]).Copy() + } + } + } + + table.patterns[d.key2redis(ts, pat)] = keys + + // Time End, Time, Peak + if d.dbStatsConfig.TableStats { + stats = d.stats.Tables[ts.Name] + } else { + stats = d.stats.AllTables + } + + stats.Hits++ + stats.GetTablePatternHits++ + if cacheHit { + stats.GetTablePatternCacheHits++ + } + + if d.dbStatsConfig.TimeStats { + dur = time.Since(now) + + if dur > stats.Peak { + stats.Peak = dur + } + stats.Time += dur + + if dur > stats.GetTablePatternPeak { + stats.GetTablePatternPeak = dur + } + stats.GetTablePatternTime += dur + } + + if d.dbStatsConfig.TableStats { + d.stats.Tables[ts.Name] = stats + } else { + d.stats.AllTables = stats + } + + return table, err +} + +//////////////////////////////////////////////////////////////////////////////// +// Internal Functions // +//////////////////////////////////////////////////////////////////////////////// + +var luaScriptGetTable *redis.Script + +func init() { + // Lua Script: + // + // Key1 Value1 Key2 Value2 ... + // f11 v11 f12 v12 ... f21 v21 ... + + luaScriptGetTable = redis.NewScript(` + local tkNv = {} + for i,k in pairs(redis.call('KEYS', KEYS[1])) do + tkNv[2*i - 1], tkNv[2*i] = k, redis.call('HGETALL', k) + end + return tkNv + `) + +} diff --git a/translib/db/db_table_pattern_test.go b/translib/db/db_table_pattern_test.go new file mode 100644 index 000000000000..80f77e4a127d --- /dev/null +++ b/translib/db/db_table_pattern_test.go @@ -0,0 +1,223 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// Copyright 2022 Broadcom. The term Broadcom refers to Broadcom Inc. and/or // +// its subsidiaries. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); // +// you may not use this file except in compliance with the License. // +// You may obtain a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +// // +//////////////////////////////////////////////////////////////////////////////// + +package db + +import ( + "fmt" + "reflect" + "testing" +) + +var allKeysPat = Key{Comp: []string{"*"}} +var singleKeyPat = Key{Comp: []string{"KEY1"}} +var genericKeyPat = Key{Comp: []string{"KEY*"}} + +func BenchmarkGetTablePattern(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, e := db.GetTablePattern(&ts, allKeysPat); e != nil { + b.Errorf("GetTablePattern() returns err: %v", e) + } + } +} + +func BenchmarkGetTableOrig(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, e := db.GetTable(&ts); e != nil { + b.Errorf("GetTable() returns err: %v", e) + } + } +} + +func TestGetTablePattern(t *testing.T) { + if _, e := db.GetTablePattern(&ts, allKeysPat); e != nil { + t.Errorf("GetTablePattern() returns err: %v", e) + } +} + +func TestGetTablePatternSingular(t *testing.T) { + if _, e := db.GetTablePattern(&ts, singleKeyPat); e != nil { + t.Errorf("GetTablePattern() returns err: %v", e) + } +} + +func TestGetTablePatternGeneric(t *testing.T) { + if _, e := db.GetTablePattern(&ts, genericKeyPat); e != nil { + t.Errorf("GetTablePattern() returns err: %v", e) + } +} + +func TestGetTablePatternEmpty(t *testing.T) { + if _, e := db.GetTablePattern(&TableSpec{Name: "UNLIKELY_23"}, allKeysPat); e != nil { + t.Errorf("GetTablePattern() returns err: %v", e) + } +} + +func TestGetTableOrig(t *testing.T) { + if _, e := db.GetTable(&ts); e != nil { + t.Errorf("GetTable() returns err: %v", e) + } +} + +func TestGetTableOrigEmpty(t *testing.T) { + if _, e := db.GetTable(&TableSpec{Name: "UNLIKELY_23"}); e != nil { + t.Errorf("GetTable() returns err: %v", e) + } +} + +func TestGetTablePatternCompOrig(t *testing.T) { + tPat, e := db.GetTablePattern(&ts, allKeysPat) + if e != nil { + t.Errorf("GetTablePattern() returns err: %v", e) + } + + tOrig, e := db.GetTable(&ts) + if e != nil { + t.Errorf("GetTable() returns err: %v", e) + } + + // The ordering of the arrays in patterns map values matters + delete(tPat.patterns, db.key2redis(&ts, Key{Comp: []string{"*"}})) + delete(tOrig.patterns, db.key2redis(&ts, Key{Comp: []string{"*"}})) + + if !reflect.DeepEqual(tPat, tOrig) { + fmt.Println("\ntPat: \n", tPat) + fmt.Println("\ntOrig: \n", tOrig) + t.Errorf("GetTable() != GetTablePattern") + } +} + +func TestGetTablePatternCompOrigEmpty(t *testing.T) { + tsEmpty := TableSpec{Name: "UNLIKELY_23"} + tPat, e := db.GetTablePattern(&tsEmpty, allKeysPat) + if e != nil { + t.Errorf("GetTablePattern() returns err: %v", e) + } + + tOrig, e := db.GetTable(&tsEmpty) + if e != nil { + t.Errorf("GetTable() returns err: %v", e) + } + + if !reflect.DeepEqual(tPat, tOrig) { + fmt.Println("\ntPat: \n", tPat) + fmt.Println("\ntOrig: \n", tOrig) + t.Errorf("Empty GetTable() != GetTablePattern") + } +} + +func TestGetTablePattern_txCache(t *testing.T) { + d := newTestDB(t, Options{ + DBNo: ConfigDB, + DisableCVLCheck: true, + }) + setupTestData(t, d.client, map[string]map[string]interface{}{ + "TEST_INTERFACE|Ethernet0": {"vrf": "Vrf1"}, + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": {"NULL": "NULL"}, + "TEST_INTERFACE|Ethernet1": {"NULL": "NULL"}, + "TEST_INTERFACE|Ethernet1|101.0.0.1/24": {"NULL": "NULL"}, + }) + + testTable := &TableSpec{Name: "TEST_INTERFACE"} + nullValue := Value{map[string]string{"NULL": "NULL"}} + vrfValue1 := Value{map[string]string{"vrf": "Vrf1"}} + + // Run few tests before transaction changes + + t.Run("T|*/beforeWrite", testGetTablePattern(d, testTable, NewKey("*"), map[string]Value{ + "TEST_INTERFACE|Ethernet0": vrfValue1, + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet1": nullValue, + "TEST_INTERFACE|Ethernet1|101.0.0.1/24": nullValue, + })) + + t.Run("T|*|*/beforeWrite", testGetTablePattern(d, testTable, NewKey("*", "*"), map[string]Value{ + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet1|101.0.0.1/24": nullValue, + })) + + t.Run("T|x|*/beforeWrite", testGetTablePattern(d, testTable, NewKey("Ethernet0", "*"), map[string]Value{ + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + })) + + t.Run("T|x|*/unknown,beforeWrite", testGetTablePattern(d, testTable, NewKey("Ethernet9", "*"), map[string]Value{})) + + // Perform few changes in a transaction + + if err := d.StartTx(nil, nil); err != nil { + t.Fatal("StartTx() failed:", err) + } + d.SetEntry(testTable, *NewKey("Ethernet1"), vrfValue1) + d.DeleteEntry(testTable, *NewKey("Ethernet1", "101.0.0.1/24")) + d.CreateEntry(testTable, *NewKey("Ethernet2"), nullValue) + d.CreateEntry(testTable, *NewKey("Ethernet2", "102.0.0.1/24"), nullValue) + d.CreateEntry(testTable, *NewKey("Ethernet2", "102.0.0.2/32"), nullValue) + + // Rerun the tests + + t.Run("T|*", testGetTablePattern(d, testTable, NewKey("*"), map[string]Value{ + "TEST_INTERFACE|Ethernet0": vrfValue1, + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet1": vrfValue1, + "TEST_INTERFACE|Ethernet2": nullValue, + "TEST_INTERFACE|Ethernet2|102.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet2|102.0.0.2/32": nullValue, + })) + + t.Run("T|*|*", testGetTablePattern(d, testTable, NewKey("*", "*"), map[string]Value{ + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet2|102.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet2|102.0.0.2/32": nullValue, + })) + + t.Run("T|*|*x", testGetTablePattern(d, testTable, NewKey("*", "*/24"), map[string]Value{ + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet2|102.0.0.1/24": nullValue, + })) + + t.Run("T|x|*/created", testGetTablePattern(d, testTable, NewKey("Ethernet2", "*"), map[string]Value{ + "TEST_INTERFACE|Ethernet2|102.0.0.1/24": nullValue, + "TEST_INTERFACE|Ethernet2|102.0.0.2/32": nullValue, + })) + + t.Run("T|x|*/deleted", testGetTablePattern(d, testTable, NewKey("Ethernet1", "*"), map[string]Value{})) + + t.Run("T|x|*/unknown", testGetTablePattern(d, testTable, NewKey("Ethernet9", "*"), map[string]Value{})) + + t.Run("T|x|*/unmodified", testGetTablePattern(d, testTable, NewKey("Ethernet0", "*"), map[string]Value{ + "TEST_INTERFACE|Ethernet0|100.0.0.1/24": nullValue, + })) +} + +func testGetTablePattern(d *DB, ts *TableSpec, pat *Key, exp map[string]Value) func(*testing.T) { + return func(t *testing.T) { + table, err := d.GetTablePattern(ts, *pat) + if err != nil { + t.Fatalf("GetTablePattern(\"%s\") returned error: %v", d.key2redis(ts, *pat), err) + } + if table.entry == nil { + table.entry = make(map[string]Value) + } + if !reflect.DeepEqual(table.entry, exp) { + t.Errorf("GetTablePattern(\"%s\") returned expected values", d.key2redis(ts, *pat)) + t.Errorf("Expecting: %v", exp) + t.Errorf("Received : %v", table.entry) + } + } +} diff --git a/translib/tlerr/tlerr.go b/translib/tlerr/tlerr.go index ea49a32854f9..6f0f2a190f4b 100644 --- a/translib/tlerr/tlerr.go +++ b/translib/tlerr/tlerr.go @@ -88,6 +88,14 @@ func (e TranslibTransactionFail) Error() string { type TranslibDBSubscribeFail struct { } +type TranslibDBScriptFail struct { + Description string +} + +func (e TranslibDBScriptFail) Error() string { + return p.Sprintf("Translib Redis Error: DB Script Fail: %s", e.Description) +} + func (e TranslibDBSubscribeFail) Error() string { return p.Sprintf("Translib Redis Error: DB Subscribe Fail") }