github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pubsub/sql/offsets_adapter_mysql.go (about) 1 package sql 2 3 import ( 4 "fmt" 5 ) 6 7 // DefaultMySQLOffsetsAdapter is adapter for storing offsets for MySQL (or MariaDB) databases. 8 // 9 // DefaultMySQLOffsetsAdapter is designed to support multiple subscribers with exactly once delivery 10 // and guaranteed order. 11 // 12 // We are using FOR UPDATE in NextOffsetQuery to lock consumer group in offsets table. 13 // 14 // When another consumer is trying to consume the same message, deadlock should occur in ConsumedMessageQuery. 15 // After deadlock, consumer will consume next message. 16 type DefaultMySQLOffsetsAdapter struct { 17 // GenerateMessagesOffsetsTableName may be used to override how the messages/offsets table name is generated. 18 GenerateMessagesOffsetsTableName func(topic string) string 19 } 20 21 func (a DefaultMySQLOffsetsAdapter) SchemaInitializingQueries(topic string) []string { 22 return []string{` 23 CREATE TABLE IF NOT EXISTS ` + a.MessagesOffsetsTable(topic) + ` ( 24 consumer_group VARCHAR(255) NOT NULL, 25 offset_acked BIGINT, 26 offset_consumed BIGINT NOT NULL, 27 PRIMARY KEY(consumer_group) 28 )`} 29 } 30 31 func (a DefaultMySQLOffsetsAdapter) AckMessageQuery(topic string, row Row, 32 consumerGroup string) (string, []any) { 33 ackQuery := `INSERT INTO ` + a.MessagesOffsetsTable(topic) + " (offset_consumed, offset_acked, consumer_group) " + 34 "VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE " + 35 "offset_consumed=VALUES(offset_consumed), offset_acked=VALUES(offset_acked)" 36 return ackQuery, []any{row.Offset, row.Offset, consumerGroup} 37 } 38 39 func (a DefaultMySQLOffsetsAdapter) NextOffsetQuery(topic, consumerGroup string) (string, []any) { 40 return `SELECT COALESCE( 41 (SELECT offset_acked 42 FROM ` + a.MessagesOffsetsTable(topic) + ` 43 WHERE consumer_group=? FOR UPDATE 44 ), 0)`, 45 []any{consumerGroup} 46 } 47 48 func (a DefaultMySQLOffsetsAdapter) MessagesOffsetsTable(topic string) string { 49 if a.GenerateMessagesOffsetsTableName != nil { 50 return a.GenerateMessagesOffsetsTableName(topic) 51 } 52 return fmt.Sprintf("`watermill_offsets_%s`", topic) 53 } 54 55 func (a DefaultMySQLOffsetsAdapter) ConsumedMessageQuery(topic string, row Row, 56 consumerGroup string, consumerULID []byte) (string, []any) { 57 // offset_consumed is not queried anywhere, it's used only to detect race conditions with NextOffsetQuery. 58 ackQuery := `INSERT INTO ` + a.MessagesOffsetsTable(topic) + ` (offset_consumed, consumer_group) 59 VALUES (?, ?) ON DUPLICATE KEY UPDATE offset_consumed=VALUES(offset_consumed)` 60 return ackQuery, []any{row.Offset, consumerGroup} 61 }