//go:build mysql // +build mysql package mysql import ( "context" "fmt" "time" "github.com/jmoiron/sqlx" "github.com/tinode/chat/server/store" t "github.com/tinode/chat/server/store/types" ) const legacyGroupMessageTable = "im_group_messages" func ensureGroupMessageShardInfrastructure(exec sqlExecutor) error { statements := []string{ `CREATE TABLE IF NOT EXISTS im_group_message_shards ( shard_month CHAR(6) NOT NULL, table_name VARCHAR(64) NOT NULL, shard_start DATETIME(3) NOT NULL, shard_end DATETIME(3) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'prepared', row_count BIGINT NOT NULL DEFAULT 0, min_created_at DATETIME(3), max_created_at DATETIME(3), created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(shard_month), UNIQUE INDEX uk_im_group_message_shards_table(table_name), INDEX idx_im_group_message_shards_status(status, shard_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_message_topic_shards ( topic VARCHAR(64) NOT NULL, shard_month CHAR(6) NOT NULL, min_seq BIGINT NOT NULL, max_seq BIGINT NOT NULL, message_count BIGINT NOT NULL DEFAULT 0, min_created_at DATETIME(3) NOT NULL, max_created_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(topic, shard_month), INDEX idx_im_group_message_topic_shards_seq(topic, max_seq), INDEX idx_im_group_message_topic_shards_month(shard_month, topic) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_message_dedup ( topic VARCHAR(64) NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64) NOT NULL, seqid BIGINT NOT NULL, shard_month CHAR(6) NOT NULL, message_id BIGINT, status VARCHAR(16) NOT NULL DEFAULT 'pending', expires_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(topic, from_user, client_msg_id), INDEX idx_im_group_message_dedup_expires(expires_at), INDEX idx_im_group_message_dedup_topic_seq(topic, seqid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, } for _, statement := range statements { if _, err := exec.Exec(statement); err != nil { return err } } return ensureGroupMessageShardWindow(exec, time.Now().UTC()) } func ensureGroupMessageShardWindow(exec sqlExecutor, now time.Time) error { first := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) for _, offset := range []int{-1, 0, 1, 2} { start := first.AddDate(0, offset, 0) status := "prepared" if offset == -1 { status = "sealing" } else if offset == 0 { status = "writable" } if err := ensureGroupMessageShard(exec, start, status); err != nil { return err } } _, err := exec.Exec("UPDATE im_group_message_shards SET status='sealed' WHERE status='sealing' AND shard_end '9' { return "", false } } parsed, err := time.Parse("200601", month) if err != nil || parsed.Format("200601") != month { return "", false } return "im_group_messages_" + month, true } func groupMessageTablesForTopic(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string) ([]string, error) { routes, cached, err := store.GetGroupMessageShardRoutes(tenantID, topic) redisAvailable := err == nil if err != nil { cached = false } if !cached { routes = nil if err = sqlx.SelectContext(ctx, q, &routes, `SELECT shard_month AS month,min_seq,max_seq,message_count AS count FROM im_group_message_topic_shards WHERE topic=? ORDER BY shard_month DESC`, topic); err != nil { return nil, err } if redisAvailable { for _, route := range routes { _ = store.PutGroupMessageShardRoute(tenantID, topic, route) } } } tables := make([]string, 0, len(routes)+1) for _, route := range routes { if table, ok := groupMessageShardTable(route.Month); ok { tables = append(tables, table) } } return append(tables, legacyGroupMessageTable), nil } func groupMessageTablesForSeq(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int64) ([]string, error) { routes, cached, err := store.GetGroupMessageShardRoutes(tenantID, topic) redisAvailable := err == nil if err != nil { cached = false } if !cached { routes = nil if err = sqlx.SelectContext(ctx, q, &routes, `SELECT shard_month AS month,min_seq,max_seq,message_count AS count FROM im_group_message_topic_shards WHERE topic=? AND min_seq<=? AND max_seq>=? ORDER BY shard_month DESC`, topic, seq, seq); err != nil { return nil, err } if redisAvailable { for _, route := range routes { _ = store.PutGroupMessageShardRoute(tenantID, topic, route) } } } tables := make([]string, 0, 2) for _, route := range routes { if route.MinSeq <= seq && route.MaxSeq >= seq { if table, ok := groupMessageShardTable(route.Month); ok { tables = append(tables, table) } } } return append(tables, legacyGroupMessageTable), nil } func groupMessageMonthFromTable(table string) string { const prefix = "im_group_messages_" if len(table) == len(prefix)+6 && table[:len(prefix)] == prefix { return table[len(prefix):] } return "" }