Compare commits

...

1 Commits

Author SHA1 Message Date
root
51adf0e7a5 fix(backend): split final UpdateAIJob + add goroutine recover() for AI job handlers
Some checks failed
Release / Test UI (push) Has been cancelled
Release / Build and push images (push) Has been cancelled
Release / Test backend (push) Has been cancelled
Release / Deploy to prod (push) Has been cancelled
Release / Deploy to homelab (push) Has been cancelled
Release / Gitea Release (push) Has been cancelled
Split the single combined UpdateAIJob call at the end of each async
goroutine into two sequential calls (status first, payload second) so
that a failure writing the large results payload does not silently
prevent the job from being marked done. Added recover() to each
goroutine to catch and log panics. Affects chapter-names, description,
and image-gen handlers.
2026-04-24 22:47:52 +05:00
2 changed files with 45 additions and 12 deletions

View File

@@ -559,6 +559,11 @@ func (s *Server) handleAdminImageGenAsync(w http.ResponseWriter, r *http.Request
go func() { go func() {
defer deregisterCancelJob(jobID) defer deregisterCancelJob(jobID)
defer jobCancel() defer jobCancel()
defer func() {
if r := recover(); r != nil {
logger.Error("admin: image-gen goroutine panic", "job_id", jobID, "recover", r)
}
}()
if jobCtx.Err() != nil { if jobCtx.Err() != nil {
_ = store.UpdateAIJob(context.Background(), jobID, map[string]any{ _ = store.UpdateAIJob(context.Background(), jobID, map[string]any{
@@ -625,13 +630,19 @@ func (s *Server) handleAdminImageGenAsync(w http.ResponseWriter, r *http.Request
Guidance: capturedReq.Guidance, Guidance: capturedReq.Guidance,
}) })
_ = store.UpdateAIJob(context.Background(), jobID, map[string]any{ if err := store.UpdateAIJob(context.Background(), jobID, map[string]any{
"status": string(domain.TaskStatusDone), "status": string(domain.TaskStatusDone),
"items_done": 1, "items_done": 1,
"items_total": 1, "items_total": 1,
"payload": string(resultJSON), "finished": time.Now().Format(time.RFC3339),
"finished": time.Now().Format(time.RFC3339), }); err != nil {
}) logger.Error("admin: image-gen failed to mark job done", "job_id", jobID, "err", err)
}
if err := store.UpdateAIJob(context.Background(), jobID, map[string]any{
"payload": string(resultJSON),
}); err != nil {
logger.Error("admin: image-gen failed to write job payload", "job_id", jobID, "err", err)
}
logger.Info("admin: image-gen async done", logger.Info("admin: image-gen async done",
"job_id", jobID, "slug", capturedReq.Slug, "job_id", jobID, "slug", capturedReq.Slug,

View File

@@ -516,6 +516,11 @@ func (s *Server) handleAdminTextGenChapterNamesAsync(w http.ResponseWriter, r *h
go func() { go func() {
defer deregisterCancelJob(jobID) defer deregisterCancelJob(jobID)
defer jobCancel() defer jobCancel()
defer func() {
if r := recover(); r != nil {
logger.Error("admin: text-gen chapter-names goroutine panic", "job_id", jobID, "recover", r)
}
}()
var allResults []proposedChapterTitle var allResults []proposedChapterTitle
chaptersDone := 0 chaptersDone := 0
@@ -574,12 +579,18 @@ func (s *Server) handleAdminTextGenChapterNamesAsync(w http.ResponseWriter, r *h
if jobCtx.Err() != nil { if jobCtx.Err() != nil {
status = domain.TaskStatusCancelled status = domain.TaskStatusCancelled
} }
_ = store.UpdateAIJob(context.Background(), jobID, map[string]any{ if err := store.UpdateAIJob(context.Background(), jobID, map[string]any{
"status": string(status), "status": string(status),
"items_done": chaptersDone, "items_done": chaptersDone,
"finished": time.Now().Format(time.RFC3339), "finished": time.Now().Format(time.RFC3339),
"payload": finalPayload, }); err != nil {
}) logger.Error("admin: text-gen chapter-names failed to mark job done", "job_id", jobID, "err", err)
}
if err := store.UpdateAIJob(context.Background(), jobID, map[string]any{
"payload": finalPayload,
}); err != nil {
logger.Error("admin: text-gen chapter-names failed to write job payload", "job_id", jobID, "err", err)
}
logger.Info("admin: text-gen chapter-names async done", logger.Info("admin: text-gen chapter-names async done",
"job_id", jobID, "slug", capturedSlug, "job_id", jobID, "slug", capturedSlug,
"results", len(allResults), "status", string(status)) "results", len(allResults), "status", string(status))
@@ -902,6 +913,11 @@ func (s *Server) handleAdminTextGenDescriptionAsync(w http.ResponseWriter, r *ht
go func() { go func() {
defer deregisterCancelJob(jobID) defer deregisterCancelJob(jobID)
defer jobCancel() defer jobCancel()
defer func() {
if r := recover(); r != nil {
logger.Error("admin: text-gen description goroutine panic", "job_id", jobID, "recover", r)
}
}()
if jobCtx.Err() != nil { if jobCtx.Err() != nil {
_ = store.UpdateAIJob(context.Background(), jobID, map[string]any{ _ = store.UpdateAIJob(context.Background(), jobID, map[string]any{
@@ -955,13 +971,19 @@ func (s *Server) handleAdminTextGenDescriptionAsync(w http.ResponseWriter, r *ht
NewDescription: strings.TrimSpace(newDesc), NewDescription: strings.TrimSpace(newDesc),
}) })
_ = store.UpdateAIJob(context.Background(), jobID, map[string]any{ if err := store.UpdateAIJob(context.Background(), jobID, map[string]any{
"status": string(domain.TaskStatusDone), "status": string(domain.TaskStatusDone),
"items_done": 1, "items_done": 1,
"items_total": 1, "items_total": 1,
"payload": string(resultJSON),
"finished": time.Now().Format(time.RFC3339), "finished": time.Now().Format(time.RFC3339),
}) }); err != nil {
logger.Error("admin: text-gen description failed to mark job done", "job_id", jobID, "err", err)
}
if err := store.UpdateAIJob(context.Background(), jobID, map[string]any{
"payload": string(resultJSON),
}); err != nil {
logger.Error("admin: text-gen description failed to write job payload", "job_id", jobID, "err", err)
}
logger.Info("admin: text-gen description async done", "job_id", jobID, "slug", capturedMeta.Slug) logger.Info("admin: text-gen description async done", "job_id", jobID, "slug", capturedMeta.Slug)
}() }()