//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" ) func ensureGroupMessageShardInfrastructure(exec sqlExecutor, tenantID t.TenantID) error { statements := []string{ `CREATE TABLE IF NOT EXISTS im_group_message_shards ( tenant_id BIGINT NOT NULL DEFAULT 0, 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(tenant_id, shard_month), UNIQUE INDEX uk_im_group_message_shards_table(tenant_id, table_name), INDEX idx_im_group_message_shards_status(tenant_id, status, shard_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_message_topic_shards ( tenant_id BIGINT NOT NULL DEFAULT 0, 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(tenant_id, topic, shard_month), INDEX idx_im_group_message_topic_shards_seq(tenant_id, topic, max_seq), INDEX idx_im_group_message_topic_shards_month(tenant_id, shard_month, topic) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, `CREATE TABLE IF NOT EXISTS im_group_message_dedup ( tenant_id BIGINT NOT NULL DEFAULT 0, 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(tenant_id, topic, from_user, client_msg_id), INDEX idx_im_group_message_dedup_expires(expires_at), INDEX idx_im_group_message_dedup_topic_seq(tenant_id, 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, tenantID, time.Now().UTC()) } func ensureGroupMessageShardWindow(exec sqlExecutor, tenantID t.TenantID, 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, tenantID, start, status); err != nil { return err } } _, err := exec.Exec("UPDATE im_group_message_shards SET status='sealed' WHERE tenant_id=? AND status='sealing' AND shard_end=? ORDER BY shard_month DESC`, tenantID, 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(tenantID, route.Month); ok { tables = append(tables, table) } } } return tables, nil } func groupMessageMonthFromTable(table string) string { const suffixLength = 6 if len(table) > suffixLength && validShardMonth(table[len(table)-suffixLength:]) { return table[len(table)-suffixLength:] } return "" }