Skip to content

Commit 544ccb3

Browse files
sredxnysredny buitrago
andauthored
TT-13421 create indexes on sharded sql pumps (#861)
* started to add indexes to shards * create indexes when SQL pump is sharded * added test for ensure index in SQL pump * update sql aggregate to handle multiple indexes on sharded analytics * update SQL aggregate test * isolate index build logic * some refactor * fixing test for indexes in SQL pump * gofmt sql_test.go * check drop table err * gofumpt * ensuring table for SQL * gofumpt * refactoring ensureIndex in sql pump * improve logging * refactor logic while looping over indexes * gofumpt * only use the waitgroup for background --------- Co-authored-by: sredny buitrago <sredny@srednys-MacBook-Pro.local>
1 parent fbcb614 commit 544ccb3

5 files changed

Lines changed: 277 additions & 16 deletions

File tree

‎analytics/analytics.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ type AnalyticsRecord struct {
5353
Month time.Month `json:"month" sql:"-"`
5454
Year int `json:"year" sql:"-"`
5555
Hour int `json:"hour" sql:"-"`
56-
ResponseCode int `json:"response_code" gorm:"column:responsecode;index"`
57-
APIKey string `json:"api_key" gorm:"column:apikey;index"`
58-
TimeStamp time.Time `json:"timestamp" gorm:"column:timestamp;index"`
56+
ResponseCode int `json:"response_code" gorm:"column:responsecode"`
57+
APIKey string `json:"api_key" gorm:"column:apikey"`
58+
TimeStamp time.Time `json:"timestamp" gorm:"column:timestamp"`
5959
APIVersion string `json:"api_version" gorm:"column:apiversion"`
6060
APIName string `json:"api_name" sql:"-"`
61-
APIID string `json:"api_id" gorm:"column:apiid;index"`
62-
OrgID string `json:"org_id" gorm:"column:orgid;index"`
63-
OauthID string `json:"oauth_id" gorm:"column:oauthid;index"`
61+
APIID string `json:"api_id" gorm:"column:apiid"`
62+
OrgID string `json:"org_id" gorm:"column:orgid"`
63+
OauthID string `json:"oauth_id" gorm:"column:oauthid"`
6464
RequestTime int64 `json:"request_time" gorm:"column:requesttime"`
6565
RawRequest string `json:"raw_request" gorm:"column:rawrequest"`
6666
RawResponse string `json:"raw_response" gorm:"column:rawresponse"`

‎pumps/sql.go‎

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"encoding/hex"
66
"errors"
77
"fmt"
8+
"sync"
9+
10+
"github.com/sirupsen/logrus"
811

912
"github.com/TykTechnologies/tyk-pump/analytics"
1013
"github.com/mitchellh/mapstructure"
@@ -45,6 +48,9 @@ type SQLPump struct {
4548
db *gorm.DB
4649
dbType string
4750
dialect gorm.Dialector
51+
52+
// this channel is used to signal that the background index creation has finished - this is used for testing
53+
backgroundIndexCreated chan bool
4854
}
4955

5056
// @PumpConf SQL
@@ -107,6 +113,18 @@ var (
107113
SQLPrefix = "SQL-pump"
108114
SQLDefaultENV = PUMPS_ENV_PREFIX + "_SQL" + PUMPS_ENV_META_PREFIX
109115
SQLDefaultQueryBatchSize = 1000
116+
117+
indexes = []struct {
118+
baseName string
119+
column string
120+
}{
121+
{"idx_responsecode", "responsecode"},
122+
{"idx_apikey", "apikey"},
123+
{"idx_timestamp", "timestamp"},
124+
{"idx_apiid", "apiid"},
125+
{"idx_orgid", "orgid"},
126+
{"idx_oauthid", "oauthid"},
127+
}
110128
)
111129

112130
func (c *SQLPump) New() Pump {
@@ -231,8 +249,8 @@ func (c *SQLPump) WriteData(ctx context.Context, data []interface{}) error {
231249

232250
table := analytics.SQLTable + "_" + recDate
233251
c.db = c.db.Table(table)
234-
if !c.db.Migrator().HasTable(table) {
235-
c.db.AutoMigrate(&analytics.AnalyticsRecord{})
252+
if errTable := c.ensureTable(table); errTable != nil {
253+
return errTable
236254
}
237255
} else {
238256
i = dataLen // write all records at once for non-sharded case, stop for loop after 1 iteration
@@ -354,3 +372,96 @@ func (c *SQLPump) WriteUptimeData(data []interface{}) {
354372

355373
c.log.Debug("Purged ", len(data), " records...")
356374
}
375+
376+
func (c *SQLPump) buildIndexName(indexBaseName, tableName string) string {
377+
return fmt.Sprintf("%s_%s", tableName, indexBaseName)
378+
}
379+
380+
func (c *SQLPump) createIndex(indexBaseName, tableName, column string) error {
381+
indexName := c.buildIndexName(indexBaseName, tableName)
382+
option := ""
383+
if c.dbType == "postgres" {
384+
option = "CONCURRENTLY"
385+
}
386+
387+
columnExist := c.db.Migrator().HasColumn(&analytics.AnalyticsRecord{}, column)
388+
if !columnExist {
389+
return errors.New("cannot create index for non existent column " + column)
390+
}
391+
392+
query := fmt.Sprintf("CREATE INDEX %s IF NOT EXISTS %s ON %s (%s)", option, indexName, tableName, column)
393+
err := c.db.Exec(query).Error
394+
if err != nil {
395+
c.log.WithFields(logrus.Fields{
396+
"index": indexName,
397+
"table": tableName,
398+
}).WithError(err).Error("Error creating index")
399+
return err
400+
}
401+
402+
c.log.Infof("Index %s created for table %s", indexName, tableName)
403+
c.log.WithFields(logrus.Fields{
404+
"index": indexName,
405+
"table": tableName,
406+
}).Info("Index created")
407+
return nil
408+
}
409+
410+
// ensureIndex check that all indexes for the analytics SQL table are in place
411+
func (c *SQLPump) ensureIndex(tableName string, background bool) error {
412+
if !c.db.Migrator().HasTable(tableName) {
413+
return errors.New("cannot create indexes as table doesn't exist: " + tableName)
414+
}
415+
416+
// waitgroup to facilitate testing and track when all indexes are created
417+
var wg sync.WaitGroup
418+
if background {
419+
wg.Add(len(indexes))
420+
}
421+
422+
for _, idx := range indexes {
423+
indexName := tableName + idx.baseName
424+
425+
if c.db.Migrator().HasIndex(tableName, indexName) {
426+
c.log.WithFields(logrus.Fields{
427+
"index": indexName,
428+
"table": tableName,
429+
}).Info("Index already exists")
430+
continue
431+
}
432+
433+
if background {
434+
go func(baseName, cols string) {
435+
defer wg.Done()
436+
if err := c.createIndex(baseName, tableName, cols); err != nil {
437+
c.log.Error(err)
438+
}
439+
}(idx.baseName, idx.column)
440+
} else {
441+
if err := c.createIndex(idx.baseName, tableName, idx.column); err != nil {
442+
return err
443+
}
444+
}
445+
}
446+
447+
if background {
448+
wg.Wait()
449+
c.backgroundIndexCreated <- true
450+
}
451+
return nil
452+
}
453+
454+
// ensureTable creates the table if it doesn't exist
455+
func (c *SQLPump) ensureTable(tableName string) error {
456+
if !c.db.Migrator().HasTable(tableName) {
457+
c.db = c.db.Table(tableName)
458+
if err := c.db.Migrator().CreateTable(&analytics.AnalyticsRecord{}); err != nil {
459+
c.log.Error("error creating table", err)
460+
return err
461+
}
462+
if err := c.ensureIndex(tableName, false); err != nil {
463+
return err
464+
}
465+
}
466+
return nil
467+
}

‎pumps/sql_aggregate.go‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,15 @@ func (c *SQLAggregatePump) ensureIndex(tableName string, background bool) error
160160
c.log.Info("omit_index_creation set to true, omitting index creation..")
161161
return nil
162162
}
163-
164-
if !c.db.Migrator().HasIndex(tableName, newAggregatedIndexName) {
163+
indexName := fmt.Sprintf("%s_%s", tableName, newAggregatedIndexName)
164+
if !c.db.Migrator().HasIndex(tableName, indexName) {
165165
createIndexFn := func(c *SQLAggregatePump) error {
166166
option := ""
167167
if c.dbType == "postgres" {
168168
option = "CONCURRENTLY"
169169
}
170170

171-
err := c.db.Table(tableName).Exec(fmt.Sprintf("CREATE INDEX %s IF NOT EXISTS %s ON %s (dimension, timestamp, org_id, dimension_value)", option, newAggregatedIndexName, tableName)).Error
171+
err := c.db.Table(tableName).Exec(fmt.Sprintf("CREATE INDEX %s IF NOT EXISTS %s ON %s (dimension, timestamp, org_id, dimension_value)", option, indexName, tableName)).Error
172172
if err != nil {
173173
c.log.Errorf("error creating index for table %s : %s", tableName, err.Error())
174174
return err
@@ -178,7 +178,7 @@ func (c *SQLAggregatePump) ensureIndex(tableName string, background bool) error
178178
c.backgroundIndexCreated <- true
179179
}
180180

181-
c.log.Info("Index ", newAggregatedIndexName, " for table ", tableName, " created successfully")
181+
c.log.Info("Index ", indexName, " for table ", tableName, " created successfully")
182182

183183
return nil
184184
}
@@ -198,7 +198,6 @@ func (c *SQLAggregatePump) ensureIndex(tableName string, background bool) error
198198
c.log.Info("Creating index for table ", tableName, "...")
199199
return createIndexFn(c)
200200
}
201-
c.log.Info(newAggregatedIndexName, " already exists.")
202201

203202
return nil
204203
}

‎pumps/sql_aggregate_test.go‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pumps
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"net/http"
78
"testing"
89
"time"
@@ -31,7 +32,8 @@ func TestSQLAggregateInit(t *testing.T) {
3132
assert.Equal(t, "sqlite", pmp.db.Dialector.Name())
3233
assert.Equal(t, true, pmp.db.Migrator().HasTable(analytics.AggregateSQLTable))
3334

34-
assert.Equal(t, true, pmp.db.Migrator().HasIndex(analytics.AggregateSQLTable, newAggregatedIndexName))
35+
indexName := fmt.Sprintf("%s_%s", analytics.AggregateSQLTable, newAggregatedIndexName)
36+
assert.Equal(t, true, pmp.db.Migrator().HasIndex(analytics.AggregateSQLTable, indexName))
3537

3638
// Checking with invalid type
3739
cfg["type"] = "invalid"
@@ -337,7 +339,7 @@ func TestDecodeRequestAndDecodeResponseSQLAggregate(t *testing.T) {
337339
assert.False(t, newPump.GetDecodedResponse())
338340
}
339341

340-
func TestEnsureIndex(t *testing.T) {
342+
func TestEnsureIndexSQLAggregate(t *testing.T) {
341343
//nolint:govet
342344
tcs := []struct {
343345
testName string
@@ -419,6 +421,44 @@ func TestEnsureIndex(t *testing.T) {
419421
expectedErr: nil,
420422
shouldHaveIndex: true,
421423
},
424+
{
425+
testName: "index created correctly, background on sharded pump",
426+
pmpSetupFn: func(tableName string) *SQLAggregatePump {
427+
pmp := &SQLAggregatePump{}
428+
cfg := &SQLAggregatePumpConf{}
429+
cfg.Type = "sqlite"
430+
cfg.TableSharding = true
431+
cfg.ConnectionString = ""
432+
pmp.SQLConf = cfg
433+
434+
pmp.log = log.WithField("prefix", "sql-aggregate-pump")
435+
dialect, errDialect := Dialect(&pmp.SQLConf.SQLConf)
436+
if errDialect != nil {
437+
return nil
438+
}
439+
db, err := gorm.Open(dialect, &gorm.Config{
440+
AutoEmbedd: true,
441+
UseJSONTags: true,
442+
Logger: logger.Default.LogMode(logger.Info),
443+
})
444+
if err != nil {
445+
return nil
446+
}
447+
pmp.db = db
448+
449+
pmp.backgroundIndexCreated = make(chan bool, 1)
450+
451+
if err := pmp.ensureTable(tableName); err != nil {
452+
return nil
453+
}
454+
455+
return pmp
456+
},
457+
givenTableName: "shard1",
458+
givenRunInBackground: true,
459+
expectedErr: nil,
460+
shouldHaveIndex: true,
461+
},
422462
{
423463
testName: "index created on non existing table, not background",
424464
pmpSetupFn: func(tableName string) *SQLAggregatePump {
@@ -499,7 +539,8 @@ func TestEnsureIndex(t *testing.T) {
499539
// wait for the background index creation to finish
500540
<-pmp.backgroundIndexCreated
501541
} else {
502-
hasIndex := pmp.db.Table(tc.givenTableName).Migrator().HasIndex(tc.givenTableName, newAggregatedIndexName)
542+
indexName := fmt.Sprintf("%s_%s", tc.givenTableName, newAggregatedIndexName)
543+
hasIndex := pmp.db.Table(tc.givenTableName).Migrator().HasIndex(tc.givenTableName, indexName)
503544
assert.Equal(t, tc.shouldHaveIndex, hasIndex)
504545
}
505546
} else {

‎pumps/sql_test.go‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,113 @@ func TestDecodeRequestAndDecodeResponseSQL(t *testing.T) {
383383
assert.False(t, newPump.GetDecodedRequest())
384384
assert.False(t, newPump.GetDecodedResponse())
385385
}
386+
387+
func setupSQLPump(t *testing.T, tableName string, useBackground bool) *SQLPump {
388+
t.Helper()
389+
pmp := &SQLPump{}
390+
pmp.log = log.WithField("prefix", "sql-pump")
391+
cfg := map[string]interface{}{
392+
"type": "sqlite",
393+
"connection_string": "",
394+
}
395+
396+
assert.NoError(t, pmp.Init(cfg))
397+
if useBackground {
398+
pmp.backgroundIndexCreated = make(chan bool, 1)
399+
}
400+
assert.NoError(t, pmp.ensureTable(tableName))
401+
402+
return pmp
403+
}
404+
405+
func TestEnsureIndexSQL(t *testing.T) {
406+
//nolint:govet
407+
tcs := []struct {
408+
testName string
409+
givenTableName string
410+
expectedErr error
411+
pmpSetupFn func(t *testing.T, tableName string) *SQLPump
412+
givenRunInBackground bool
413+
shouldHaveIndex bool
414+
}{
415+
{
416+
testName: "index created correctly, not background",
417+
pmpSetupFn: func(t *testing.T, tableName string) *SQLPump {
418+
return setupSQLPump(t, tableName, false)
419+
},
420+
givenTableName: "analytics_no_background",
421+
givenRunInBackground: false,
422+
expectedErr: nil,
423+
shouldHaveIndex: true,
424+
},
425+
{
426+
testName: "index created correctly, background",
427+
pmpSetupFn: func(t *testing.T, tableName string) *SQLPump {
428+
return setupSQLPump(t, tableName, true)
429+
},
430+
givenTableName: "analytics_background",
431+
givenRunInBackground: true,
432+
expectedErr: nil,
433+
shouldHaveIndex: true,
434+
},
435+
}
436+
437+
for _, tc := range tcs {
438+
t.Run(tc.testName, func(t *testing.T) {
439+
pmp := tc.pmpSetupFn(t, tc.givenTableName)
440+
defer func() {
441+
err := pmp.db.Migrator().DropTable(tc.givenTableName)
442+
if err != nil {
443+
t.Errorf("Failed to drop table: %v", err)
444+
}
445+
}()
446+
assert.NotNil(t, pmp)
447+
448+
actualErr := pmp.ensureIndex(tc.givenTableName, tc.givenRunInBackground)
449+
isErrExpected := tc.expectedErr != nil
450+
didErr := actualErr != nil
451+
assert.Equal(t, isErrExpected, didErr)
452+
453+
if isErrExpected {
454+
assert.Equal(t, tc.expectedErr.Error(), actualErr.Error())
455+
}
456+
457+
if actualErr == nil {
458+
if tc.givenRunInBackground {
459+
// wait for the background index creation to finish
460+
<-pmp.backgroundIndexCreated
461+
}
462+
463+
indexToUse := indexes[0]
464+
indexName := pmp.buildIndexName(indexToUse.baseName, tc.givenTableName)
465+
hasIndex := pmp.db.Table(tc.givenTableName).Migrator().HasIndex(tc.givenTableName, indexName)
466+
assert.Equal(t, tc.shouldHaveIndex, hasIndex)
467+
}
468+
})
469+
}
470+
}
471+
472+
func TestBuildIndexName(t *testing.T) {
473+
tests := []struct {
474+
indexBaseName string
475+
tableName string
476+
expected string
477+
}{
478+
{"idx_responsecode", "users", "users_idx_responsecode"},
479+
{"idx_apikey", "transactions", "transactions_idx_apikey"},
480+
{"idx_timestamp", "logs", "logs_idx_timestamp"},
481+
{"idx_apiid", "api_calls", "api_calls_idx_apiid"},
482+
{"idx_orgid", "organizations", "organizations_idx_orgid"},
483+
}
484+
485+
c := &SQLPump{} // Create an instance of SQLPump.
486+
487+
for _, tt := range tests {
488+
t.Run(tt.indexBaseName+"_"+tt.tableName, func(t *testing.T) {
489+
result := c.buildIndexName(tt.indexBaseName, tt.tableName)
490+
if result != tt.expected {
491+
t.Errorf("buildIndexName(%s, %s) = %s; want %s", tt.indexBaseName, tt.tableName, result, tt.expected)
492+
}
493+
})
494+
}
495+
}

0 commit comments

Comments
 (0)