{"record":{"id":"8b9f55dc6566dc38","repo":"Billionmail/BillionMail","slug":"enqueue-video-job-w","errorCode":null,"errorMessage":"enqueue video job: %w","messagePattern":"enqueue video job: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/orchestrator.go","lineNumber":87,"sourceCode":"\t\t`)\n\t\tif err != nil {\n\t\t\tg.Log().Warning(ctx, \"create bm_video_jobs table: \", err)\n\t\t}\n\t})\n}\n\n// EnqueueVideoJob inserts a pending job into bm_video_jobs.\nfunc EnqueueVideoJob(ctx context.Context, contactID int, email string, groupID int) (int, error) {\n\tensureTable(ctx)\n\n\tresult, err := g.DB().Model(\"bm_video_jobs\").Ctx(ctx).Insert(g.Map{\n\t\t\"contact_id\":    contactID,\n\t\t\"contact_email\": email,\n\t\t\"group_id\":      groupID,\n\t\t\"status\":        JobPending,\n\t})\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"enqueue video job: %w\", err)\n\t}\n\tid, _ := result.LastInsertId()\n\treturn int(id), nil\n}\n\n// ProcessVideoJobs polls for pending jobs and launches pipelines.\n// Called by gtimer every 30s.\nfunc ProcessVideoJobs(ctx context.Context) {\n\tensureTable(ctx)\n\n\tvar jobs []VideoJob\n\terr := g.DB().Model(\"bm_video_jobs\").Ctx(ctx).\n\t\tWhere(\"status\", JobPending).\n\t\tOrderAsc(\"id\").\n\t\tLimit(maxConcurrentJobs).\n\t\tScan(&jobs)\n\tif err != nil {\n\t\tg.Log().Warning(ctx, \"query pending video jobs: \", err)","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/orchestrator.go#L69-L105","documentation":"EnqueueVideoJob inserts a pending row into the bm_video_jobs table via GoFrame's g.DB(). When the INSERT fails (connection error, missing table, constraint violation), the raw DB error is wrapped with 'enqueue video job: %w'. Note ensureTable only logs failures with g.Log().Warning — if table creation silently failed, the very next Insert fails here with 'relation \"bm_video_jobs\" does not exist'.","triggerScenarios":"g.DB().Model(\"bm_video_jobs\").Insert(...) errors: DB unreachable/wrong credentials, bm_video_jobs missing because ensureTable's CREATE TABLE failed earlier, schema drift (a required column absent after a migration), or a Postgres error such as invalid input for contact_email/group_id.","commonSituations":"Fresh deployment where the DB user lacks CREATE privilege so ensureTable's warning was missed; Postgres down or misconfigured DSN in config; manual schema changes dropping the table; migration created the table in a different schema/search_path; connection pool exhausted under bulk enqueues from GenerateVideo.","solutions":["Read the wrapped (%w) DB error — 'relation does not exist' means run the CREATE TABLE manually or fix the ensureTable failure path.","Check DB connectivity and the GoFrame DB config (host, port, user, password, database) with a direct psql connection.","Grant the app DB user CREATE/INSERT privileges on the schema, or pre-create bm_video_jobs in your migration set instead of relying on ensureTable.","Make ensureTable fail loudly (return an error) instead of only logging a warning, so this failure surfaces at startup.","Verify search_path/schema matches between the migration and the runtime connection."],"exampleFix":"// before\nfunc ensureTable(ctx context.Context) {\n\ttableOnce.Do(func() {\n\t\t_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS bm_video_jobs (...)`)\n\t\tif err != nil {\n\t\t\tg.Log().Warning(ctx, \"create bm_video_jobs table: \", err)\n\t\t}\n\t})\n}\n// after\n// pre-create the table in a migration; at runtime fail fast on DB errors:\nresult, err := g.DB().Model(\"bm_video_jobs\").Ctx(ctx).Insert(g.Map{...})\nif err != nil {\n\treturn 0, fmt.Errorf(\"enqueue video job: %w\", err) // now wrapped error points at real DB cause\n}","handlingStrategy":"try-catch","validationCode":"// before enqueueing: verify table exists and DB is reachable\nctxT, cancel := context.WithTimeout(ctx, 5*time.Second)\ndefer cancel()\nif _, err := g.DB().Exec(ctxT, \"SELECT 1 FROM bm_video_jobs LIMIT 1\"); err != nil {\n\t// table missing or DB down — surface immediately instead of failing at Insert\n\treturn fmt.Errorf(\"bm_video_jobs not ready: %w\", err)\n}","typeGuard":"func isMissingTable(err error) bool {\n\treturn err != nil && strings.Contains(strings.ToLower(err.Error()), \"does not exist\")\n}\nfunc isConnectionError(err error) bool {\n\treturn errors.Is(err, context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), \"connection refused\")\n}","tryCatchPattern":"jobID, err := EnqueueVideoJob(ctx, contactID, email, groupID)\nif err != nil {\n\tswitch {\n\tcase isMissingTable(err):\n\t\t// run migration / create table explicitly, then retry once\n\tcase isConnectionError(err):\n\t\t// check DB config/health, retry after backoff\n\t}\n\treturn fmt.Errorf(\"enqueue video job: %w\", err)\n}","preventionTips":["Pre-create bm_video_jobs in a proper migration rather than relying on ensureTable's best-effort sync.Once","Change ensureTable to return/propagate errors instead of only logging a Warning","Check LastInsertId error too — with Postgres it's often unsupported; prefer RETURNING id","Validate DB config and connectivity as a startup health check before accepting jobs","Grant the app role INSERT (and CREATE if ensureTable stays) on the target schema"],"tags":["database","postgres","goframe","insert"],"backgroundTag":"database-insert-failed","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}