-
Notifications
You must be signed in to change notification settings - Fork 20
👌 feat(message:deduplication) implementing the feature #33 #229
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
base: master
Are you sure you want to change the base?
Changes from 3 commits
ea27b72
4a79cab
979554b
c2d8b8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package implementation | ||
|
||
import ( | ||
"database/sql" | ||
"time" | ||
|
||
"emperror.dev/errors" | ||
"github.com/sirupsen/logrus" | ||
|
||
"github.com/wework/grabbit/gbus" | ||
"github.com/wework/grabbit/gbus/deduplicator" | ||
"github.com/wework/grabbit/gbus/tx" | ||
) | ||
|
||
var _ deduplicator.Store = &deduper{} | ||
|
||
type deduper struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpicking, the struct is named deduper which is the main struct here yet the file name is tx.go |
||
*gbus.Glogged | ||
svcName string | ||
policy gbus.DeduplicationPolicy | ||
txProvider gbus.TxProvider | ||
age time.Duration | ||
ticker *time.Ticker | ||
done chan bool | ||
tableName string | ||
} | ||
|
||
func (d *deduper) Purge() (err error) { | ||
truncateSQL := "TRUNCATE TABLE " + d.tableName | ||
rhinof marked this conversation as resolved.
Show resolved
Hide resolved
|
||
txp, err := d.txProvider.New() | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { | ||
if err != nil { | ||
serr := txp.Rollback() | ||
err = errors.Append(err, serr) | ||
} | ||
err = txp.Commit() | ||
}() | ||
_, err = txp.Exec(truncateSQL) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (d *deduper) Start() { | ||
d.ticker = time.NewTicker(time.Minute) | ||
d.done = make(chan bool) | ||
deleteQuery := "DELETE FROM " + d.tableName + " WHERE `created_at` < ?" | ||
go func() { | ||
for { | ||
select { | ||
case <-d.done: | ||
return | ||
case <-d.ticker.C: | ||
oldest := time.Now().Add(-1 * d.age) | ||
tx, err := d.txProvider.New() | ||
if err != nil { | ||
d.Log().WithError(err).Error("failed to acquire a tx") | ||
continue | ||
} | ||
result, err := tx.Exec(deleteQuery, oldest) | ||
if err != nil && err != sql.ErrNoRows { | ||
d.Log().WithError(err).Error("failed executing delete query") | ||
vladshub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
n, err := result.RowsAffected() | ||
if err != nil { | ||
d.Log().WithError(err).Error("failed to get count of affected rows") | ||
} else { | ||
d.Log().WithField("table_name", d.tableName).WithField("rows_deleted", n). | ||
Info("successfully cleanup duplicates table") | ||
} | ||
} | ||
} | ||
}() | ||
} | ||
|
||
func (d *deduper) Stop() { | ||
d.Log().Info("shutting down deduplicator") | ||
d.ticker.Stop() | ||
close(d.done) | ||
} | ||
|
||
// | ||
func (d *deduper) StoreMessageID(tx *sql.Tx, id string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. did you consider to store the message in another storage mechanism? like redis etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't want to introduce another infrastructure dependency but we can have different storages that would implement the same interface. |
||
insertSQL := "INSERT INTO " + d.tableName + " (id) values (?)" | ||
_, err := tx.Exec(insertSQL, id) | ||
if err != nil { | ||
d.Log().WithError(err).Error("failed to insert the id of the message into the dedup table") | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// MessageExists checks if a message id is in the deduplication table and returns an error if it fails | ||
func (d *deduper) MessageExists(id string) (bool, error) { | ||
if d.policy == gbus.DeduplicationPolicyNone { | ||
return false, nil | ||
} | ||
tx, err := d.txProvider.New() | ||
if err != nil { | ||
return true, err | ||
} | ||
defer func() { | ||
err = tx.Rollback() | ||
if err != nil { | ||
d.Log().WithError(err).Error("could not commit tx for query MessageExists") | ||
} | ||
}() | ||
selectSQL := "SELECT EXISTS (SELECT id FROM " + d.tableName + " WHERE id = ? limit 1)" | ||
|
||
var exists bool | ||
err = tx.QueryRow(selectSQL, id).Scan(&exists) | ||
if err != nil && err == sql.ErrNoRows { | ||
return false, nil | ||
} | ||
|
||
if err != nil { | ||
return true, err | ||
} | ||
|
||
return exists, nil | ||
} | ||
|
||
func NewDeduplicator(svcName string, policy gbus.DeduplicationPolicy, txProvider gbus.TxProvider, age time.Duration, logger logrus.FieldLogger) deduplicator.Store { | ||
d := &deduper{ | ||
svcName: svcName, | ||
policy: policy, | ||
txProvider: txProvider, | ||
age: age, | ||
tableName: tx.GrabbitTableNameTemplate(svcName, "duplicates"), | ||
} | ||
l := logger.WithField("grabbit", "deduplicator") | ||
d.SetLogger(l) | ||
return d | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package deduplicator | ||
|
||
import ( | ||
"database/sql" | ||
) | ||
|
||
// Store abstracts the way deduplicateor manages the | ||
type Store interface { | ||
vladshub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
StoreMessageID(tx *sql.Tx, id string) error | ||
MessageExists(id string) (bool, error) | ||
Purge() error | ||
Start() | ||
Stop() | ||
} |
Uh oh!
There was an error while loading. Please reload this page.