Add configurable concurrency to collection directory backfillUnverified
a19fa48 parent: b8b7aa8 modified
internal/server/server.go +34 -21 | @@ -11,6 +11,7 @@ import ( | ||
| 11 | 11 | "slices" |
| 12 | 12 | "strconv" |
| 13 | 13 | "strings" |
| 14 | + "sync" | |
| 14 | 15 | "time" |
| 15 | 16 | |
| 16 | 17 | "github.com/go-chi/chi/v5" |
| @@ -441,7 +442,7 @@ func (s *Server) PeriodicSync(ctx context.Context, interval time.Duration) { | ||
| 441 | 442 | } |
| 442 | 443 | } |
| 443 | 444 | |
| 444 | -func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL string) { | |
| 445 | +func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL string, concurrency int) { | |
| 445 | 446 | if collectionDirURL == "" { |
| 446 | 447 | return |
| 447 | 448 | } |
| @@ -469,36 +470,48 @@ func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL | ||
| 469 | 470 | |
| 470 | 471 | s.logger.Info("collection directory backfill", "total", len(dids), "missing", len(missing)) |
| 471 | 472 | |
| 473 | + sem := make(chan struct{}, concurrency) | |
| 474 | + var wg sync.WaitGroup | |
| 475 | + | |
| 472 | 476 | for _, did := range missing { |
| 473 | 477 | if ctx.Err() != nil { |
| 474 | - return | |
| 478 | + break | |
| 475 | 479 | } |
| 476 | 480 | |
| 477 | - handle := did | |
| 478 | - if ident, err := atproto.ResolveIdentity(ctx, did); err == nil { | |
| 479 | - handle = ident.Handle.String() | |
| 480 | - } | |
| 481 | + sem <- struct{}{} | |
| 482 | + wg.Add(1) | |
| 481 | 483 | |
| 482 | - if _, err := s.db.CreateUser(ctx, did, handle, "", ""); err != nil { | |
| 483 | - s.logger.Error("failed to create user during backfill", "error", err, "did", did) | |
| 484 | - continue | |
| 485 | - } | |
| 484 | + go func(did string) { | |
| 485 | + defer func() { <-sem }() | |
| 486 | + defer wg.Done() | |
| 486 | 487 | |
| 487 | - pdsURL, err := atproto.ResolvePDSEndpoint(ctx, did) | |
| 488 | - if err != nil { | |
| 489 | - s.logger.Error("failed to resolve PDS for backfill", "error", err, "did", did) | |
| 490 | - continue | |
| 491 | - } | |
| 488 | + handle := did | |
| 489 | + if ident, err := atproto.ResolveIdentity(ctx, did); err == nil { | |
| 490 | + handle = ident.Handle.String() | |
| 491 | + } | |
| 492 | 492 | |
| 493 | - client := atproto.NewUnauthenticatedClient(pdsURL) | |
| 494 | - sync := atproto.NewSync(s.db, client, s.logger) | |
| 495 | - if err := sync.Run(ctx, did); err != nil { | |
| 496 | - s.logger.Error("backfill sync failed", "error", err, "did", did) | |
| 497 | - } | |
| 493 | + if _, err := s.db.CreateUser(ctx, did, handle, "", ""); err != nil { | |
| 494 | + s.logger.Error("failed to create user during backfill", "error", err, "did", did) | |
| 495 | + return | |
| 496 | + } | |
| 497 | + | |
| 498 | + pdsURL, err := atproto.ResolvePDSEndpoint(ctx, did) | |
| 499 | + if err != nil { | |
| 500 | + s.logger.Error("failed to resolve PDS for backfill", "error", err, "did", did) | |
| 501 | + return | |
| 502 | + } | |
| 503 | + | |
| 504 | + client := atproto.NewUnauthenticatedClient(pdsURL) | |
| 505 | + sync := atproto.NewSync(s.db, client, s.logger) | |
| 506 | + if err := sync.Run(ctx, did); err != nil { | |
| 507 | + s.logger.Error("backfill sync failed", "error", err, "did", did) | |
| 508 | + } | |
| 498 | 509 | |
| 499 | - s.refreshUserFeeds(ctx, did) | |
| 510 | + s.refreshUserFeeds(ctx, did) | |
| 511 | + }(did) | |
| 500 | 512 | } |
| 501 | 513 | |
| 514 | + wg.Wait() | |
| 502 | 515 | s.logger.Info("collection directory backfill complete") |
| 503 | 516 | } |
| 504 | 517 | |
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | "slices" | 11 | "slices" |
| 12 | "strconv" | 12 | "strconv" |
| 13 | "strings" | 13 | "strings" |
| 14 | + "sync" | ||
| 14 | "time" | 15 | "time" |
| 15 | 16 | ||
| 16 | "github.com/go-chi/chi/v5" | 17 | "github.com/go-chi/chi/v5" |
| @@ -441,7 +442,7 @@ func (s *Server) PeriodicSync(ctx context.Context, interval time.Duration) { | |||
| 441 | } | 442 | } |
| 442 | } | 443 | } |
| 443 | 444 | ||
| 444 | -func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL string) { | 445 | +func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL string, concurrency int) { |
| 445 | if collectionDirURL == "" { | 446 | if collectionDirURL == "" { |
| 446 | return | 447 | return |
| 447 | } | 448 | } |
| @@ -469,36 +470,48 @@ func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL | |||
| 469 | 470 | ||
| 470 | s.logger.Info("collection directory backfill", "total", len(dids), "missing", len(missing)) | 471 | s.logger.Info("collection directory backfill", "total", len(dids), "missing", len(missing)) |
| 471 | 472 | ||
| 473 | + sem := make(chan struct{}, concurrency) | ||
| 474 | + var wg sync.WaitGroup | ||
| 475 | + | ||
| 472 | for _, did := range missing { | 476 | for _, did := range missing { |
| 473 | if ctx.Err() != nil { | 477 | if ctx.Err() != nil { |
| 474 | - return | 478 | + break |
| 475 | } | 479 | } |
| 476 | 480 | ||
| 477 | - handle := did | 481 | + sem <- struct{}{} |
| 478 | - if ident, err := atproto.ResolveIdentity(ctx, did); err == nil { | 482 | + wg.Add(1) |
| 479 | - handle = ident.Handle.String() | ||
| 480 | - } | ||
| 481 | 483 | ||
| 482 | - if _, err := s.db.CreateUser(ctx, did, handle, "", ""); err != nil { | 484 | + go func(did string) { |
| 483 | - s.logger.Error("failed to create user during backfill", "error", err, "did", did) | 485 | + defer func() { <-sem }() |
| 484 | - continue | 486 | + defer wg.Done() |
| 485 | - } | ||
| 486 | 487 | ||
| 487 | - pdsURL, err := atproto.ResolvePDSEndpoint(ctx, did) | 488 | + handle := did |
| 488 | - if err != nil { | 489 | + if ident, err := atproto.ResolveIdentity(ctx, did); err == nil { |
| 489 | - s.logger.Error("failed to resolve PDS for backfill", "error", err, "did", did) | 490 | + handle = ident.Handle.String() |
| 490 | - continue | 491 | + } |
| 491 | - } | ||
| 492 | 492 | ||
| 493 | - client := atproto.NewUnauthenticatedClient(pdsURL) | 493 | + if _, err := s.db.CreateUser(ctx, did, handle, "", ""); err != nil { |
| 494 | - sync := atproto.NewSync(s.db, client, s.logger) | 494 | + s.logger.Error("failed to create user during backfill", "error", err, "did", did) |
| 495 | - if err := sync.Run(ctx, did); err != nil { | 495 | + return |
| 496 | - s.logger.Error("backfill sync failed", "error", err, "did", did) | 496 | + } |
| 497 | - } | 497 | + |
| 498 | + pdsURL, err := atproto.ResolvePDSEndpoint(ctx, did) | ||
| 499 | + if err != nil { | ||
| 500 | + s.logger.Error("failed to resolve PDS for backfill", "error", err, "did", did) | ||
| 501 | + return | ||
| 502 | + } | ||
| 503 | + | ||
| 504 | + client := atproto.NewUnauthenticatedClient(pdsURL) | ||
| 505 | + sync := atproto.NewSync(s.db, client, s.logger) | ||
| 506 | + if err := sync.Run(ctx, did); err != nil { | ||
| 507 | + s.logger.Error("backfill sync failed", "error", err, "did", did) | ||
| 508 | + } | ||
| 498 | 509 | ||
| 499 | - s.refreshUserFeeds(ctx, did) | 510 | + s.refreshUserFeeds(ctx, did) |
| 511 | + }(did) | ||
| 500 | } | 512 | } |
| 501 | 513 | ||
| 514 | + wg.Wait() | ||
| 502 | s.logger.Info("collection directory backfill complete") | 515 | s.logger.Info("collection directory backfill complete") |
| 503 | } | 516 | } |
| 504 | 517 | ||
modified
main.go +12 -1 | @@ -8,6 +8,7 @@ import ( | ||
| 8 | 8 | "net/http" |
| 9 | 9 | "os" |
| 10 | 10 | "os/signal" |
| 11 | + "strconv" | |
| 11 | 12 | "syscall" |
| 12 | 13 | "time" |
| 13 | 14 | |
| @@ -25,6 +26,7 @@ func main() { | ||
| 25 | 26 | syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval") |
| 26 | 27 | clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 10*time.Minute), "cluster recomputation interval") |
| 27 | 28 | collectionDirURL := flag.String("collection-dir", envOr("GLEAN_COLLECTION_DIR_URL", ""), "collection directory URL for startup backfill") |
| 29 | + backfillConcurrency := flag.Int("backfill-concurrency", envInt("GLEAN_BACKFILL_CONCURRENCY", 5), "max concurrent backfill workers") | |
| 28 | 30 | flag.Parse() |
| 29 | 31 | |
| 30 | 32 | atproto.InitIdentity(envOr("GLEAN_PLC_URL", "https://didplc.glean.at")) |
| @@ -70,7 +72,7 @@ func main() { | ||
| 70 | 72 | srv.PeriodicSync(ctx, *syncInterval) |
| 71 | 73 | }() |
| 72 | 74 | go func() { |
| 73 | - srv.BackfillFromCollectionDir(ctx, *collectionDirURL) | |
| 75 | + srv.BackfillFromCollectionDir(ctx, *collectionDirURL, *backfillConcurrency) | |
| 74 | 76 | }() |
| 75 | 77 | go func() { |
| 76 | 78 | if err := jetstream.Start(ctx); err != nil && ctx.Err() == nil { |
| @@ -125,3 +127,12 @@ func envDuration(key string, fallback time.Duration) time.Duration { | ||
| 125 | 127 | } |
| 126 | 128 | return fallback |
| 127 | 129 | } |
| 130 | + | |
| 131 | +func envInt(key string, fallback int) int { | |
| 132 | + if v := os.Getenv(key); v != "" { | |
| 133 | + if n, err := strconv.Atoi(v); err == nil { | |
| 134 | + return n | |
| 135 | + } | |
| 136 | + } | |
| 137 | + return fallback | |
| 138 | +} | |
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | "net/http" | 8 | "net/http" |
| 9 | "os" | 9 | "os" |
| 10 | "os/signal" | 10 | "os/signal" |
| 11 | + "strconv" | ||
| 11 | "syscall" | 12 | "syscall" |
| 12 | "time" | 13 | "time" |
| 13 | 14 | ||
| @@ -25,6 +26,7 @@ func main() { | |||
| 25 | syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval") | 26 | syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval") |
| 26 | clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 10*time.Minute), "cluster recomputation interval") | 27 | clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 10*time.Minute), "cluster recomputation interval") |
| 27 | collectionDirURL := flag.String("collection-dir", envOr("GLEAN_COLLECTION_DIR_URL", ""), "collection directory URL for startup backfill") | 28 | collectionDirURL := flag.String("collection-dir", envOr("GLEAN_COLLECTION_DIR_URL", ""), "collection directory URL for startup backfill") |
| 29 | + backfillConcurrency := flag.Int("backfill-concurrency", envInt("GLEAN_BACKFILL_CONCURRENCY", 5), "max concurrent backfill workers") | ||
| 28 | flag.Parse() | 30 | flag.Parse() |
| 29 | 31 | ||
| 30 | atproto.InitIdentity(envOr("GLEAN_PLC_URL", "https://didplc.glean.at")) | 32 | atproto.InitIdentity(envOr("GLEAN_PLC_URL", "https://didplc.glean.at")) |
| @@ -70,7 +72,7 @@ func main() { | |||
| 70 | srv.PeriodicSync(ctx, *syncInterval) | 72 | srv.PeriodicSync(ctx, *syncInterval) |
| 71 | }() | 73 | }() |
| 72 | go func() { | 74 | go func() { |
| 73 | - srv.BackfillFromCollectionDir(ctx, *collectionDirURL) | 75 | + srv.BackfillFromCollectionDir(ctx, *collectionDirURL, *backfillConcurrency) |
| 74 | }() | 76 | }() |
| 75 | go func() { | 77 | go func() { |
| 76 | if err := jetstream.Start(ctx); err != nil && ctx.Err() == nil { | 78 | if err := jetstream.Start(ctx); err != nil && ctx.Err() == nil { |
| @@ -125,3 +127,12 @@ func envDuration(key string, fallback time.Duration) time.Duration { | |||
| 125 | } | 127 | } |
| 126 | return fallback | 128 | return fallback |
| 127 | } | 129 | } |
| 130 | + | ||
| 131 | +func envInt(key string, fallback int) int { | ||
| 132 | + if v := os.Getenv(key); v != "" { | ||
| 133 | + if n, err := strconv.Atoi(v); err == nil { | ||
| 134 | + return n | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + return fallback | ||
| 138 | +} | ||