{"record":{"id":"d4d9b038a113f313","repo":"sipeed/picoclaw","slug":"job-not-found","errorCode":null,"errorMessage":"job not found","messagePattern":"job not found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/cron/service.go","lineNumber":487,"sourceCode":"\t\t\tprevious := cs.store.Jobs[i]\n\t\t\tupdated := cloneCronJob(*job)\n\t\t\tnow := time.Now().UnixMilli()\n\t\t\tupdated.UpdatedAtMS = now\n\t\t\tif updated.Enabled {\n\t\t\t\tif previous.Enabled != updated.Enabled || !sameSchedule(previous.Schedule, updated.Schedule) {\n\t\t\t\t\tupdated.State.NextRunAtMS = cs.computeNextRun(&updated.Schedule, now)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tupdated.State.NextRunAtMS = nil\n\t\t\t}\n\t\t\tcs.store.Jobs[i] = updated\n\n\t\t\tcs.notify()\n\n\t\t\treturn cs.saveStoreUnsafe()\n\t\t}\n\t}\n\treturn fmt.Errorf(\"job not found\")\n}\n\nfunc cloneCronJob(job CronJob) CronJob {\n\tclone := job\n\tif job.Schedule.AtMS != nil {\n\t\tatMS := *job.Schedule.AtMS\n\t\tclone.Schedule.AtMS = &atMS\n\t}\n\tif job.Schedule.EveryMS != nil {\n\t\teveryMS := *job.Schedule.EveryMS\n\t\tclone.Schedule.EveryMS = &everyMS\n\t}\n\tif job.State.NextRunAtMS != nil {\n\t\tnextRunAtMS := *job.State.NextRunAtMS\n\t\tclone.State.NextRunAtMS = &nextRunAtMS\n\t}\n\tif job.State.LastRunAtMS != nil {\n\t\tlastRunAtMS := *job.State.LastRunAtMS","sourceCodeStart":469,"sourceCodeEnd":505,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/cron/service.go#L469-L505","documentation":"UpdateJob iterates cs.store.Jobs looking for a job whose ID equals job.ID; if none matches it returns this error without modifying anything. It is the library's not-found sentinel for the update path. Note it also fires when Start() was never called (store is nil/empty) and after a one-shot 'at' job auto-deleted itself (DeleteAfterRun).","triggerScenarios":"UpdateJob(&CronJob{ID: \"...\"}) where the ID came from a stale ListJobs snapshot and another goroutine already RemoveJob'd it; updating a schedule.Kind==\"at\" job that already ran and was auto-deleted; job.ID empty (zero-value struct); service restarted against a different storePath so old IDs are gone.","commonSituations":"UI holding a cached job list while the job is removed elsewhere; retrying an update against a restarted service; passing a job copy from a previous deployment; racing RemoveJob vs UpdateJob in concurrent handlers.","solutions":["Re-fetch the current ID right before updating: GetJob(jobID) and treat ok==false as expected (job is gone - drop it from your UI/queue)","If the job should exist, verify you are talking to the same service/store: check storePath and ListJobs output","For one-shot 'at' jobs, expect this error after execution - they delete themselves; use AddJob to reschedule","Never reuse zero-value CronJob structs; always start from a GetJob/ListJobs copy and mutate fields you intend to change"],"exampleFix":"// before: blind update with a possibly stale copy\nif err := svc.UpdateJob(&staleJob); err != nil {\n    return err // \"job not found\" surprises callers\n}\n\n// after: guard with GetJob and degrade gracefully\nif _, ok := svc.GetJob(staleJob.ID); !ok {\n    // job removed or one-shot already fired; nothing to update\n    return nil\n}\nif err := svc.UpdateJob(&staleJob); err != nil {\n    if strings.Contains(err.Error(), \"job not found\") {\n        return nil // lost a race with RemoveJob - acceptable\n    }\n    return err\n}","handlingStrategy":"validation","validationCode":"// Re-validate immediately before updating; narrows the race window to near zero.\nif _, ok := svc.GetJob(job.ID); !ok {\n    // job gone (removed or one-shot 'at' already fired): nothing to update\n    return nil\n}\nif err := svc.UpdateJob(job); err != nil { ... }","typeGuard":"func isJobNotFound(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"job not found\")\n}","tryCatchPattern":"if err := svc.UpdateJob(job); err != nil {\n    if isJobNotFound(err) {\n        // expected after auto-delete of one-shot jobs or a concurrent RemoveJob;\n        // refresh your job list rather than surfacing an error\n        return refreshJobs()\n    }\n    return err\n}","preventionTips":["Always copy the job from GetJob/ListJobs immediately before UpdateJob; never cache IDs across turns","Remember schedule.Kind==\"at\" jobs self-delete after running - updates after fire will 404 by design","Treat job-not-found as an expected outcome in UIs: remove the row instead of showing an error","Confirm the service instance/storePath matches where the job was created before assuming a bug"],"tags":["cron","not-found","api","state","concurrency"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}