{"record":{"id":"28e0fa6c43068eb9","repo":"getzola/zola","slug":"couldn-t-lock-imageproc-num-img-ops","errorCode":null,"errorMessage":"Couldn't lock imageproc (num_img_ops)","messagePattern":"Couldn't lock imageproc \\(num_img_ops\\)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"components/site/src/lib.rs","lineNumber":686,"sourceCode":"                false,\n                None,\n            )?;\n        }\n        // We're fine with missing static folders\n        if self.static_path.exists() {\n            copy_directory(\n                &self.static_path,\n                &self.output_path,\n                self.config.hard_link_static,\n                self.config.ignored_static_globset.as_ref(),\n            )?;\n        }\n\n        Ok(())\n    }\n\n    pub fn num_img_ops(&self) -> usize {\n        let imageproc = self.imageproc.lock().expect(\"Couldn't lock imageproc (num_img_ops)\");\n        imageproc.num_img_ops()\n    }\n\n    pub fn process_images(&self) -> Result<()> {\n        let mut imageproc =\n            self.imageproc.lock().expect(\"Couldn't lock imageproc (process_images)\");\n        imageproc.prune()?;\n        imageproc.do_process()\n    }\n\n    /// Deletes the `public` directory if it exists and the `preserve_dotfiles_in_output` option is set to false,\n    /// or if set to true: its contents except for the dotfiles at the root level.\n    pub fn clean(&self) -> Result<()> {\n        clean_site_output_folder(&self.output_path, self.config.preserve_dotfiles_in_output)\n    }\n\n    fn copy_assets(&self, parent: &Path, assets: &[impl AsRef<Path>], dest: &Path) -> Result<()> {\n        for asset in assets {","sourceCodeStart":668,"sourceCodeEnd":704,"githubUrl":"https://github.com/getzola/zola/blob/61d30828217957120309db657950224edf9707e2/components/site/src/lib.rs#L668-L704","documentation":"Site::num_img_ops locks the shared imageproc Mutex to report the number of pending image operations, panicking with this expect message if the lock cannot be acquired. As with other imageproc lock sites, this only happens when the mutex is poisoned by a panic in another thread holding it.","triggerScenarios":"Calling num_img_ops after the imageproc mutex was poisoned by a panic elsewhere (e.g. during process_images or a template-driven image operation).","commonSituations":"Build tooling that reports image op counts after a partial/failed build in which an image worker panicked.","solutions":["Fix the root-cause panic that poisoned the mutex; check earlier log output for the original backtrace.","Use unwrap_or_else(|p| p.into_inner()) to tolerate poisoning when reading the count.","Avoid invoking num_img_ops once a build failure is known.","Serialize access so image-processing panics are handled before the count is queried."],"exampleFix":"// before\nlet imageproc = self.imageproc.lock().expect(\"Couldn't lock imageproc (num_img_ops)\");\n// after\nlet imageproc = self.imageproc.lock().unwrap_or_else(|p| p.into_inner());","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let n = match site.imageproc.lock() {\n    Ok(g) => g.num_img_ops(),\n    Err(poisoned) => poisoned.into_inner().num_img_ops(),\n};","preventionTips":["Query counts only after a successful build phase.","Handle build errors so worker panics don't poison shared state.","Restart the build/server after any panic.","Wrap image ops to return Result rather than panicking."],"tags":["mutex","poisoned-lock","concurrency","panic"],"backgroundTag":"mutex-poisoned","analyzedSha":"61d30828217957120309db657950224edf9707e2","analyzedAt":"2026-09-03T14:39:09.727Z","contentChangedAt":"2026-09-03T14:39:09.727Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}