thread-master/apps/backend/cmd/init/main.go

164 lines
8.7 KiB
Go
Raw Normal View History

2026-07-13 08:59:13 +00:00
package main
import (
"context"
"flag"
"fmt"
"time"
"apps/backend/internal/config"
"github.com/zeromicro/go-zero/core/conf"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// init creates idempotent operational indexes only. It intentionally never
// inserts members or other business records.
func main() {
configFile := flag.String("f", "etc/gateway.yaml", "config file")
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
c.ApplyEnv()
if c.Mongo.URI == "" || c.Mongo.Database == "" {
panic("Mongo.URI and Mongo.Database are required")
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(c.Mongo.URI))
if err != nil {
panic(err)
}
defer client.Disconnect(context.Background())
db := client.Database(c.Mongo.Database)
2026-07-28 06:33:40 +00:00
for collection, models := range indexModels() {
if _, err := db.Collection(collection).Indexes().CreateMany(ctx, models); err != nil {
panic(fmt.Errorf("create %s indexes: %w", collection, err))
}
}
fmt.Println("database indexes initialized")
}
// ownerIndex covers the near-universal "list one member's rows, newest first" shape. The sort key
// belongs in the index too, otherwise Mongo fetches every matching row before sorting.
func ownerIndex(sortField, name string) mongo.IndexModel {
return mongo.IndexModel{
Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: sortField, Value: -1}},
Options: options.Index().SetName(name),
}
}
// indexModels lists every index the running code depends on. Owner-scoped collections were all
// unindexed, so each list endpoint was a full collection scan that grew with total rows across
// every member rather than with the member's own data.
//
// These are deliberately all non-unique. Adding a uniqueness constraint to existing data can make
// CreateMany fail and take a deploy down, so that belongs in its own migration with a duplicate
// pre-check rather than here.
func indexModels() map[string][]mongo.IndexModel {
return map[string][]mongo.IndexModel{
// members.uid is read on every authenticated request via the auth middleware, and it is a
// field lookup rather than _id, so without this it scanned the whole collection whenever
// the Redis cache missed.
"members": {
{Keys: bson.D{{Key: "uid", Value: 1}}, Options: options.Index().SetName("member_uid")},
{Keys: bson.D{{Key: "email", Value: 1}}, Options: options.Index().SetName("member_email")},
{Keys: bson.D{{Key: "phone", Value: 1}}, Options: options.Index().SetName("member_phone")},
},
"identities": {
{Keys: bson.D{{Key: "login_id", Value: 1}, {Key: "platform", Value: 1}}, Options: options.Index().SetName("identity_login_platform")},
},
"member_settings": {
{Keys: bson.D{{Key: "uid", Value: 1}}, Options: options.Index().SetName("settings_uid")},
},
2026-07-13 08:59:13 +00:00
"jobs": {
{Keys: bson.D{{Key: "status", Value: 1}, {Key: "run_after", Value: 1}, {Key: "created_at", Value: 1}}, Options: options.Index().SetName("claim_due_jobs")},
{Keys: bson.D{{Key: "status", Value: 1}, {Key: "completed_at", Value: 1}}, Options: options.Index().SetName("purge_terminal_jobs")},
2026-07-28 06:33:40 +00:00
// The jobs list is polled continuously by the UI and runs a count plus a find per poll.
ownerIndex("updated_at", "owner_jobs_updated"),
},
// One notification is written per job state change and nothing purges them, so this
// collection grows without bound and its scan cost grows with it.
"notifications": {
ownerIndex("created_at", "owner_notifications_created"),
},
"threads_accounts": {
ownerIndex("created_at", "owner_accounts_created"),
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "threads_user_id", Value: 1}}, Options: options.Index().SetName("owner_threads_user")},
2026-07-13 08:59:13 +00:00
},
"studio_outbox": {
{Keys: bson.D{{Key: "status", Value: 1}, {Key: "updated_at", Value: 1}}, Options: options.Index().SetName("worker_outbox_status_updated")},
2026-07-15 15:23:59 +00:00
{Keys: bson.D{{Key: "steps.status", Value: 1}, {Key: "steps.scheduled_at", Value: 1}, {Key: "steps.lease_expires_at", Value: 1}}, Options: options.Index().SetName("claim_due_outbox_steps")},
2026-07-28 06:33:40 +00:00
ownerIndex("updated_at", "owner_outbox_updated"),
},
"studio_personas": {ownerIndex("updated_at", "owner_personas_updated")},
"studio_plays": {ownerIndex("updated_at", "owner_plays_updated")},
"studio_own_posts": {
ownerIndex("published_at", "owner_own_posts_published"),
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "account_id", Value: 1}, {Key: "published_at", Value: -1}}, Options: options.Index().SetName("owner_account_own_posts")},
},
"studio_mentions": {
ownerIndex("created_at", "owner_mentions_created"),
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "account_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("owner_account_mentions")},
},
"scout_brands": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_brands")}},
"scout_products": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_products")}},
"scout_posts": {
ownerIndex("created_at", "owner_posts_created"),
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "brand_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("owner_brand_posts")},
},
"scout_homework": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_homework")}},
"inspire_elements": {ownerIndex("updated_at", "owner_elements_updated")},
"inspire_sessions": {ownerIndex("updated_at", "owner_sessions_updated")},
"usage_events": {
{Keys: bson.D{{Key: "uid", Value: 1}, {Key: "month_key", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("uid_month_events")},
{Keys: bson.D{{Key: "month_key", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("tenant_month_events")},
{Keys: bson.D{{Key: "created_at", Value: 1}}, Options: options.Index().SetName("events_created_range")},
},
"usage_prefs": {
{Keys: bson.D{{Key: "uid", Value: 1}}, Options: options.Index().SetName("prefs_uid")},
// Stripe webhooks arrive knowing only the customer, so this is the reverse lookup
// back to a member and it runs on every billing event.
{Keys: bson.D{{Key: "stripe_customer_id", Value: 1}}, Options: options.Index().SetName("prefs_stripe_customer")},
},
"usage_purchases": {
{Keys: bson.D{{Key: "uid", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("uid_purchases_created")},
{Keys: bson.D{{Key: "stripe_event_id", Value: 1}}, Options: options.Index().SetName("purchase_stripe_event")},
},
"growth_outcomes": {
// The observe worker scans by status across all tenants on every tick.
{Keys: bson.D{{Key: "status", Value: 1}}, Options: options.Index().SetName("observe_status")},
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "source_type", Value: 1}, {Key: "source_id", Value: 1}}, Options: options.Index().SetName("owner_outcome_source")},
ownerIndex("created_at", "owner_outcomes_created"),
},
"growth_checkups": {ownerIndex("created_at", "owner_checkups_created")},
"growth_account_health": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_health")}},
"growth_workspaces": {{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "archived", Value: 1}}, Options: options.Index().SetName("owner_workspaces_archived")}},
"growth_draft_reviews": {
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "ref_type", Value: 1}, {Key: "ref_id", Value: 1}, {Key: "updated_at", Value: -1}}, Options: options.Index().SetName("owner_review_ref")},
},
"growth_invite_rewards": {
{Keys: bson.D{{Key: "inviter_uid", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("inviter_rewards_created")},
{Keys: bson.D{{Key: "invitee_uid", Value: 1}}, Options: options.Index().SetName("invitee_reward")},
},
"growth_utm_links": {
// Looked up by code on the unauthenticated public redirect.
{Keys: bson.D{{Key: "code", Value: 1}}, Options: options.Index().SetName("utm_code")},
ownerIndex("created_at", "owner_utm_created"),
},
"growth_ws_members": {
{Keys: bson.D{{Key: "workspace_id", Value: 1}, {Key: "uid", Value: 1}}, Options: options.Index().SetName("workspace_member")},
},
"billing_checkout_attempts": {
{Keys: bson.D{{Key: "uid", Value: 1}, {Key: "request_id", Value: 1}}, Options: options.Index().SetName("uid_request")},
{Keys: bson.D{{Key: "session_id", Value: 1}}, Options: options.Index().SetName("checkout_session")},
{Keys: bson.D{{Key: "customer_id", Value: 1}}, Options: options.Index().SetName("checkout_customer")},
2026-07-13 08:59:13 +00:00
},
}
}