Refactor follow distance computation to use SQL queriesUnverified
d5f9944 parent: d97639c modified
internal/cluster/social.go +51 -43 | @@ -3,20 +3,17 @@ package cluster | ||
| 3 | 3 | import ( |
| 4 | 4 | "context" |
| 5 | 5 | "fmt" |
| 6 | - "iter" | |
| 7 | 6 | ) |
| 8 | 7 | |
| 9 | 8 | const maxFollowDepth = 3 |
| 10 | 9 | |
| 11 | -func chunk[T any](s []T, size int) iter.Seq[[]T] { | |
| 12 | - return func(yield func([]T) bool) { | |
| 13 | - for i := 0; i < len(s); i += size { | |
| 14 | - end := min(i+size, len(s)) | |
| 15 | - if !yield(s[i:end]) { | |
| 16 | - return | |
| 17 | - } | |
| 18 | - } | |
| 10 | +func chunk[T any](s []T, size int) [][]T { | |
| 11 | + var chunks [][]T | |
| 12 | + for i := 0; i < len(s); i += size { | |
| 13 | + end := min(i+size, len(s)) | |
| 14 | + chunks = append(chunks, s[i:end]) | |
| 19 | 15 | } |
| 16 | + return chunks | |
| 20 | 17 | } |
| 21 | 18 | |
| 22 | 19 | type followDistance struct { |
| @@ -30,49 +27,60 @@ func (e *Engine) ComputeFollowDistancesData(ctx context.Context, sources []strin | ||
| 30 | 27 | return nil, nil |
| 31 | 28 | } |
| 32 | 29 | |
| 33 | - rows, err := e.db.QueryContext(ctx, `SELECT user_did, target_did FROM main.follows WHERE user_did != target_did`) | |
| 34 | - if err != nil { | |
| 35 | - return nil, err | |
| 30 | + type pair struct { | |
| 31 | + src, dst string | |
| 36 | 32 | } |
| 37 | - defer rows.Close() | |
| 33 | + distances := make(map[pair]int) | |
| 38 | 34 | |
| 39 | - adj := make(map[string][]string) | |
| 40 | - for rows.Next() { | |
| 41 | - var src, dst string | |
| 42 | - if err := rows.Scan(&src, &dst); err != nil { | |
| 43 | - return nil, err | |
| 44 | - } | |
| 45 | - adj[src] = append(adj[src], dst) | |
| 46 | - } | |
| 47 | - if err := rows.Err(); err != nil { | |
| 48 | - return nil, err | |
| 49 | - } | |
| 50 | - | |
| 51 | - var result []followDistance | |
| 52 | 35 | for _, src := range sources { |
| 53 | - dist := map[string]int{src: 0} | |
| 54 | - queue := []string{src} | |
| 55 | - for len(queue) > 0 { | |
| 56 | - cur := queue[0] | |
| 57 | - queue = queue[1:] | |
| 58 | - d := dist[cur] | |
| 59 | - if d >= maxFollowDepth { | |
| 60 | - continue | |
| 61 | - } | |
| 62 | - for _, next := range adj[cur] { | |
| 63 | - if _, ok := dist[next]; !ok { | |
| 64 | - dist[next] = d + 1 | |
| 65 | - queue = append(queue, next) | |
| 36 | + frontier := []string{src} | |
| 37 | + reachable := map[string]int{src: 0} | |
| 38 | + | |
| 39 | + for depth := 0; depth < maxFollowDepth && len(frontier) > 0; depth++ { | |
| 40 | + var nextLevel []string | |
| 41 | + for _, batch := range chunk(frontier, 500) { | |
| 42 | + ph := make([]string, len(batch)) | |
| 43 | + args := make([]any, len(batch)) | |
| 44 | + for i, did := range batch { | |
| 45 | + ph[i] = "?" | |
| 46 | + args[i] = did | |
| 47 | + } | |
| 48 | + | |
| 49 | + rows, err := e.db.QueryContext(ctx, | |
| 50 | + fmt.Sprintf(`SELECT target_did FROM main.follows WHERE user_did IN (%s) AND user_did != target_did`, joinPh(ph)), | |
| 51 | + args..., | |
| 52 | + ) | |
| 53 | + if err != nil { | |
| 54 | + return nil, err | |
| 55 | + } | |
| 56 | + | |
| 57 | + for rows.Next() { | |
| 58 | + var dst string | |
| 59 | + if err := rows.Scan(&dst); err != nil { | |
| 60 | + rows.Close() | |
| 61 | + return nil, err | |
| 62 | + } | |
| 63 | + if _, ok := reachable[dst]; !ok { | |
| 64 | + reachable[dst] = depth + 1 | |
| 65 | + nextLevel = append(nextLevel, dst) | |
| 66 | + } | |
| 66 | 67 | } |
| 68 | + rows.Close() | |
| 67 | 69 | } |
| 70 | + frontier = nextLevel | |
| 68 | 71 | } |
| 69 | - for other, d := range dist { | |
| 72 | + | |
| 73 | + for other, d := range reachable { | |
| 70 | 74 | if d > 0 { |
| 71 | - result = append(result, followDistance{userA: src, userB: other, distance: d}) | |
| 75 | + distances[pair{src, other}] = d | |
| 72 | 76 | } |
| 73 | 77 | } |
| 74 | 78 | } |
| 75 | 79 | |
| 80 | + var result []followDistance | |
| 81 | + for k, d := range distances { | |
| 82 | + result = append(result, followDistance{userA: k.src, userB: k.dst, distance: d}) | |
| 83 | + } | |
| 76 | 84 | return result, nil |
| 77 | 85 | } |
| 78 | 86 | |
| @@ -138,7 +146,7 @@ func (e *Engine) ComputeFollowDistances(ctx context.Context) error { | ||
| 138 | 146 | defer func() { _ = tx.Rollback() }() |
| 139 | 147 | |
| 140 | 148 | const sqliteMaxVars = 500 |
| 141 | - for chunk := range chunk(dirtyUsers, sqliteMaxVars) { | |
| 149 | + for _, chunk := range chunk(dirtyUsers, sqliteMaxVars) { | |
| 142 | 150 | ph := make([]string, len(chunk)) |
| 143 | 151 | args := make([]any, len(chunk)) |
| 144 | 152 | for i, did := range chunk { |
| @@ -165,7 +173,7 @@ func (e *Engine) ComputeFollowDistances(ctx context.Context) error { | ||
| 165 | 173 | } |
| 166 | 174 | } |
| 167 | 175 | |
| 168 | - for chunk := range chunk(dirtyUsers, sqliteMaxVars) { | |
| 176 | + for _, chunk := range chunk(dirtyUsers, sqliteMaxVars) { | |
| 169 | 177 | ph := make([]string, len(chunk)) |
| 170 | 178 | args := make([]any, len(chunk)) |
| 171 | 179 | for i, did := range chunk { |
| @@ -3,20 +3,17 @@ package cluster | |||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | "fmt" | 5 | "fmt" |
| 6 | - "iter" | ||
| 7 | ) | 6 | ) |
| 8 | 7 | ||
| 9 | const maxFollowDepth = 3 | 8 | const maxFollowDepth = 3 |
| 10 | 9 | ||
| 11 | -func chunk[T any](s []T, size int) iter.Seq[[]T] { | 10 | +func chunk[T any](s []T, size int) [][]T { |
| 12 | - return func(yield func([]T) bool) { | 11 | + var chunks [][]T |
| 13 | - for i := 0; i < len(s); i += size { | 12 | + for i := 0; i < len(s); i += size { |
| 14 | - end := min(i+size, len(s)) | 13 | + end := min(i+size, len(s)) |
| 15 | - if !yield(s[i:end]) { | 14 | + chunks = append(chunks, s[i:end]) |
| 16 | - return | ||
| 17 | - } | ||
| 18 | - } | ||
| 19 | } | 15 | } |
| 16 | + return chunks | ||
| 20 | } | 17 | } |
| 21 | 18 | ||
| 22 | type followDistance struct { | 19 | type followDistance struct { |
| @@ -30,49 +27,60 @@ func (e *Engine) ComputeFollowDistancesData(ctx context.Context, sources []strin | |||
| 30 | return nil, nil | 27 | return nil, nil |
| 31 | } | 28 | } |
| 32 | 29 | ||
| 33 | - rows, err := e.db.QueryContext(ctx, `SELECT user_did, target_did FROM main.follows WHERE user_did != target_did`) | 30 | + type pair struct { |
| 34 | - if err != nil { | 31 | + src, dst string |
| 35 | - return nil, err | ||
| 36 | } | 32 | } |
| 37 | - defer rows.Close() | 33 | + distances := make(map[pair]int) |
| 38 | 34 | ||
| 39 | - adj := make(map[string][]string) | ||
| 40 | - for rows.Next() { | ||
| 41 | - var src, dst string | ||
| 42 | - if err := rows.Scan(&src, &dst); err != nil { | ||
| 43 | - return nil, err | ||
| 44 | - } | ||
| 45 | - adj[src] = append(adj[src], dst) | ||
| 46 | - } | ||
| 47 | - if err := rows.Err(); err != nil { | ||
| 48 | - return nil, err | ||
| 49 | - } | ||
| 50 | - | ||
| 51 | - var result []followDistance | ||
| 52 | for _, src := range sources { | 35 | for _, src := range sources { |
| 53 | - dist := map[string]int{src: 0} | 36 | + frontier := []string{src} |
| 54 | - queue := []string{src} | 37 | + reachable := map[string]int{src: 0} |
| 55 | - for len(queue) > 0 { | 38 | + |
| 56 | - cur := queue[0] | 39 | + for depth := 0; depth < maxFollowDepth && len(frontier) > 0; depth++ { |
| 57 | - queue = queue[1:] | 40 | + var nextLevel []string |
| 58 | - d := dist[cur] | 41 | + for _, batch := range chunk(frontier, 500) { |
| 59 | - if d >= maxFollowDepth { | 42 | + ph := make([]string, len(batch)) |
| 60 | - continue | 43 | + args := make([]any, len(batch)) |
| 61 | - } | 44 | + for i, did := range batch { |
| 62 | - for _, next := range adj[cur] { | 45 | + ph[i] = "?" |
| 63 | - if _, ok := dist[next]; !ok { | 46 | + args[i] = did |
| 64 | - dist[next] = d + 1 | 47 | + } |
| 65 | - queue = append(queue, next) | 48 | + |
| 49 | + rows, err := e.db.QueryContext(ctx, | ||
| 50 | + fmt.Sprintf(`SELECT target_did FROM main.follows WHERE user_did IN (%s) AND user_did != target_did`, joinPh(ph)), | ||
| 51 | + args..., | ||
| 52 | + ) | ||
| 53 | + if err != nil { | ||
| 54 | + return nil, err | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + for rows.Next() { | ||
| 58 | + var dst string | ||
| 59 | + if err := rows.Scan(&dst); err != nil { | ||
| 60 | + rows.Close() | ||
| 61 | + return nil, err | ||
| 62 | + } | ||
| 63 | + if _, ok := reachable[dst]; !ok { | ||
| 64 | + reachable[dst] = depth + 1 | ||
| 65 | + nextLevel = append(nextLevel, dst) | ||
| 66 | + } | ||
| 66 | } | 67 | } |
| 68 | + rows.Close() | ||
| 67 | } | 69 | } |
| 70 | + frontier = nextLevel | ||
| 68 | } | 71 | } |
| 69 | - for other, d := range dist { | 72 | + |
| 73 | + for other, d := range reachable { | ||
| 70 | if d > 0 { | 74 | if d > 0 { |
| 71 | - result = append(result, followDistance{userA: src, userB: other, distance: d}) | 75 | + distances[pair{src, other}] = d |
| 72 | } | 76 | } |
| 73 | } | 77 | } |
| 74 | } | 78 | } |
| 75 | 79 | ||
| 80 | + var result []followDistance | ||
| 81 | + for k, d := range distances { | ||
| 82 | + result = append(result, followDistance{userA: k.src, userB: k.dst, distance: d}) | ||
| 83 | + } | ||
| 76 | return result, nil | 84 | return result, nil |
| 77 | } | 85 | } |
| 78 | 86 | ||
| @@ -138,7 +146,7 @@ func (e *Engine) ComputeFollowDistances(ctx context.Context) error { | |||
| 138 | defer func() { _ = tx.Rollback() }() | 146 | defer func() { _ = tx.Rollback() }() |
| 139 | 147 | ||
| 140 | const sqliteMaxVars = 500 | 148 | const sqliteMaxVars = 500 |
| 141 | - for chunk := range chunk(dirtyUsers, sqliteMaxVars) { | 149 | + for _, chunk := range chunk(dirtyUsers, sqliteMaxVars) { |
| 142 | ph := make([]string, len(chunk)) | 150 | ph := make([]string, len(chunk)) |
| 143 | args := make([]any, len(chunk)) | 151 | args := make([]any, len(chunk)) |
| 144 | for i, did := range chunk { | 152 | for i, did := range chunk { |
| @@ -165,7 +173,7 @@ func (e *Engine) ComputeFollowDistances(ctx context.Context) error { | |||
| 165 | } | 173 | } |
| 166 | } | 174 | } |
| 167 | 175 | ||
| 168 | - for chunk := range chunk(dirtyUsers, sqliteMaxVars) { | 176 | + for _, chunk := range chunk(dirtyUsers, sqliteMaxVars) { |
| 169 | ph := make([]string, len(chunk)) | 177 | ph := make([]string, len(chunk)) |
| 170 | args := make([]any, len(chunk)) | 178 | args := make([]any, len(chunk)) |
| 171 | for i, did := range chunk { | 179 | for i, did := range chunk { |
modified
internal/db/db.go +1 -0 | @@ -340,6 +340,7 @@ var articlesSchema = []string{ | ||
| 340 | 340 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed ON subscriptions(feed_url)`, |
| 341 | 341 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, |
| 342 | 342 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user ON subscriptions(user_did)`, |
| 343 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user_feed ON subscriptions(user_did, feed_url)`, | |
| 343 | 344 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)`, |
| 344 | 345 | `CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, |
| 345 | 346 | `CREATE INDEX IF NOT EXISTS articles.idx_articles_feed ON articles(feed_url)`, |
| @@ -340,6 +340,7 @@ var articlesSchema = []string{ | |||
| 340 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed ON subscriptions(feed_url)`, | 340 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed ON subscriptions(feed_url)`, |
| 341 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, | 341 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, |
| 342 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user ON subscriptions(user_did)`, | 342 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user ON subscriptions(user_did)`, |
| 343 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user_feed ON subscriptions(user_did, feed_url)`, | ||
| 343 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)`, | 344 | `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)`, |
| 344 | `CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, | 345 | `CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, |
| 345 | `CREATE INDEX IF NOT EXISTS articles.idx_articles_feed ON articles(feed_url)`, | 346 | `CREATE INDEX IF NOT EXISTS articles.idx_articles_feed ON articles(feed_url)`, |