From 3550a4e2e2b53582b02d7ff875c236594698e449 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sun, 23 Aug 2026 00:11:52 +0200 Subject: [PATCH] Harden vectorization task against per-package failures A single failing package used to kill its whole worker thread silently because only Vectorizer::Error was rescued per package. Now every StandardError is caught and logged with class and backtrace, and the embedding write is narrowed to update_column(:embedding) so the task only ever issues a minimal UPDATE - it cannot touch any other column or fire model callbacks. Progress stats no longer serialize database writes through the mutex. --- app/models/package.rb | 5 ++--- lib/tasks/vectorize_packages.rake | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/models/package.rb b/app/models/package.rb index 71b89c7..c2e31de 100644 --- a/app/models/package.rb +++ b/app/models/package.rb @@ -56,13 +56,12 @@ class Package < ApplicationRecord # Compute and store the embedding vector for this package by asking # the external vector service. Raises Vectorizer::Error on failure. - # Returns true if the vector was saved. + # Returns true if a vector was saved. def update_embedding! text = embedding_text return false if text.blank? - self.embedding = Vectorizer.embed(text) - save! + update_column(:embedding, Vectorizer.embed(text)) end # Return a relation of packages whose stored embeddings are closest to diff --git a/lib/tasks/vectorize_packages.rake b/lib/tasks/vectorize_packages.rake index 1c19837..ce873d8 100644 --- a/lib/tasks/vectorize_packages.rake +++ b/lib/tasks/vectorize_packages.rake @@ -1,5 +1,8 @@ # frozen_string_literal: true +# Long rake task bodies are normal; the block-length cop is meant for +# application code. +# rubocop:disable Metrics/BlockLength namespace :debshots do desc 'Compute embedding vectors for all packages based on their description ' \ 'and long description (missing ones only unless FORCE=1). ' \ @@ -34,17 +37,20 @@ namespace :debshots do stats = { done: 0, failed: 0 } process_package = lambda do |package| + begin + raise ArgumentError, 'no description text' if package.embedding_text.blank? + + package.update_embedding! + stats_mutex.synchronize { stats[:done] += 1 } + rescue StandardError => e + # A single broken package must never take down a whole worker + # thread - log enough detail to diagnose it and move on. + stats_mutex.synchronize { stats[:failed] += 1 } + logger.error "Failed for package #{package.name}: #{e.class}: #{e.message}" + logger.error e.backtrace.first(3).join("\n") if e.backtrace + end + stats_mutex.synchronize do - begin - raise ArgumentError, 'no description text' if package.embedding_text.blank? - - package.update_embedding! - stats[:done] += 1 - rescue Vectorizer::Error, ArgumentError => e - stats[:failed] += 1 - logger.error "Failed for package #{package.name}: #{e.message}" - end - processed = stats[:done] + stats[:failed] return unless (processed % 100).zero?