From 12174b6f20b53933279ebcf0d941ddc9aa627001 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 8 Sep 2025 22:44:54 +0200 Subject: [PATCH] Persist VNS calculation progress whenever an improvement has been made --- .../tables_arrangements_controller.rb | 11 +++++--- app/jobs/table_simulator_job.rb | 25 +++++++++++++++++-- app/models/tables_arrangement.rb | 1 + app/services/tables/distribution.rb | 12 ++++++--- app/services/vns/engine.rb | 2 +- bin/jobs | 1 + ...119_add_progress_to_tables_arrangements.rb | 5 ++++ db/schema.rb | 3 ++- db/seeds.rb | 4 ++- nginx.conf | 5 ++++ 10 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20250908145119_add_progress_to_tables_arrangements.rb diff --git a/app/controllers/tables_arrangements_controller.rb b/app/controllers/tables_arrangements_controller.rb index 9d2243a..184b577 100644 --- a/app/controllers/tables_arrangements_controller.rb +++ b/app/controllers/tables_arrangements_controller.rb @@ -9,10 +9,10 @@ class TablesArrangementsController < ApplicationController render json: TablesArrangement .order(valid: :desc) .order(discomfort: :asc) - .select(:id, :name, :discomfort, :status) - .select("digest = '#{current_digest}'::uuid as valid") + .select(:id, :name, :discomfort, :status, :progress) + .select("digest = '#{current_digest}'::uuid OR discomfort IS NULL as valid") .limit(20) - .as_json(only: %i[id name discomfort valid status]) + .as_json(only: %i[id name discomfort valid status progress]) end def show @@ -25,7 +25,10 @@ class TablesArrangementsController < ApplicationController end def create - TableSimulatorJob.perform_later(current_tenant.id) + ActiveRecord::Base.transaction do + tables_arrangement = TablesArrangement.create!(status: :not_started) + TableSimulatorJob.perform_later(current_tenant.id, tables_arrangement.id) + end render json: {}, status: :created end diff --git a/app/jobs/table_simulator_job.rb b/app/jobs/table_simulator_job.rb index 0471795..44d4759 100644 --- a/app/jobs/table_simulator_job.rb +++ b/app/jobs/table_simulator_job.rb @@ -8,16 +8,35 @@ class TableSimulatorJob < ApplicationJob MIN_PER_TABLE = 8 MAX_PER_TABLE = 10 - def perform(wedding_id) + def perform(wedding_id, tables_arrangement_id) + Rails.logger.info "Starting table simulation #{tables_arrangement_id} for wedding #{wedding_id}" ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do engine = VNS::Engine.new engine.add_optimization(Tables::Swap) engine.add_optimization(Tables::Shift) - initial_solution = Tables::Distribution.new(min_per_table: MIN_PER_TABLE, max_per_table: MAX_PER_TABLE) + tables_arrangement = TablesArrangement.find(tables_arrangement_id) + + initial_solution = Tables::Distribution.new( + min_per_table: MIN_PER_TABLE, + max_per_table: MAX_PER_TABLE, + tables_arrangement_id: + ) + initial_solution.random_distribution(Guest.potential.shuffle) + initial_solution.save! + + engine.notify_progress do |current_progress| + tables_arrangement.update_columns(status: :in_progress, progress: current_progress) + end + + engine.on_better_solution do |better_solution| + better_solution.save! + tables_arrangement.update_columns(discomfort: better_solution.discomfort) # TODO: remove? + end + engine.initial_solution = initial_solution engine.target_function(&:discomfort) @@ -25,6 +44,8 @@ class TableSimulatorJob < ApplicationJob best_solution = engine.run best_solution.save! + + tables_arrangement.update_columns(status: :completed) end end end diff --git a/app/models/tables_arrangement.rb b/app/models/tables_arrangement.rb index a0d4bb2..99f8c7c 100644 --- a/app/models/tables_arrangement.rb +++ b/app/models/tables_arrangement.rb @@ -10,6 +10,7 @@ # digest :uuid not null # discomfort :integer # name :string not null +# progress :float default(0.0), not null # status :string default("complete"), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/services/tables/distribution.rb b/app/services/tables/distribution.rb index bad8bf1..922d184 100644 --- a/app/services/tables/distribution.rb +++ b/app/services/tables/distribution.rb @@ -14,7 +14,7 @@ module Tables attr_accessor :tables, :min_per_table, :max_per_table, :hierarchy, :tables_arrangement_id - def initialize(min_per_table:, max_per_table:, hierarchy: AffinityGroupsHierarchy.new, tables_arrangement_id: nil) + def initialize(min_per_table:, max_per_table:, tables_arrangement_id:, hierarchy: AffinityGroupsHierarchy.new) @min_per_table = min_per_table @max_per_table = max_per_table @hierarchy = hierarchy @@ -43,15 +43,19 @@ module Tables end def deep_dup - self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table, - hierarchy: @hierarchy).tap do |new_distribution| + self.class.new( + min_per_table: @min_per_table, + max_per_table: @max_per_table, + hierarchy: @hierarchy, + tables_arrangement_id: @tables_arrangement_id + ).tap do |new_distribution| new_distribution.tables = @tables.map(&:dup) end end def save! ActiveRecord::Base.transaction do - arrangement = TablesArrangement.find_or_create_by!(id: tables_arrangement_id) + arrangement = TablesArrangement.find(tables_arrangement_id) self.tables_arrangement_id = arrangement.id diff --git a/app/services/vns/engine.rb b/app/services/vns/engine.rb index a5f680c..5408bc9 100644 --- a/app/services/vns/engine.rb +++ b/app/services/vns/engine.rb @@ -53,7 +53,7 @@ module VNS best_solution = @current_solution - ITERATIONS.times do |iteration| + (1..ITERATIONS).each do |iteration| @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample) @best_score = @target_function.call(@current_solution) Rails.logger.debug { "After perturbation: #{@best_score}" } diff --git a/bin/jobs b/bin/jobs index dcf59f3..fcc5d90 100755 --- a/bin/jobs +++ b/bin/jobs @@ -3,4 +3,5 @@ require_relative "../config/environment" require "solid_queue/cli" +SolidQueue.logger = ActiveSupport::Logger.new($stdout) SolidQueue::Cli.start(ARGV) diff --git a/db/migrate/20250908145119_add_progress_to_tables_arrangements.rb b/db/migrate/20250908145119_add_progress_to_tables_arrangements.rb new file mode 100644 index 0000000..2783aa2 --- /dev/null +++ b/db/migrate/20250908145119_add_progress_to_tables_arrangements.rb @@ -0,0 +1,5 @@ +class AddProgressToTablesArrangements < ActiveRecord::Migration[8.0] + def change + add_column :tables_arrangements, :progress, :float, default: 0, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index b0279ad..fb57903 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_01_102437) do +ActiveRecord::Schema[8.0].define(version: 2025_09_08_145119) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -217,6 +217,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_01_102437) do t.uuid "wedding_id", null: false t.uuid "digest", default: -> { "gen_random_uuid()" }, null: false t.string "status", default: "complete", null: false + t.float "progress", default: 0.0, null: false t.index ["wedding_id"], name: "index_tables_arrangements_on_wedding_id" end diff --git a/db/seeds.rb b/db/seeds.rb index f69b6e9..c1214ea 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -86,7 +86,9 @@ ActsAsTenant.with_tenant(wedding) do # TODO: Clean up invitations with no guests - ActiveJob.perform_all_later(3.times.map { TableSimulatorJob.new(wedding.id) }) + 3.times { TablesArrangement.create! } + .map { |arrangement| TableSimulatorJob.new(wedding.id, arrangement.id) } + .then { |jobs| ActiveJob.perform_all_later } "red".dup.paint.palette.triad(as: :hex).zip(Group.roots).each { |(color, group)| group.update!(color: color.paint.desaturate(40)) } diff --git a/nginx.conf b/nginx.conf index 58c2bb9..07f73fd 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,6 +12,11 @@ server { proxy_set_header Host $http_host; } + location /jobs/ { + proxy_pass http://backend:3000/jobs/; + proxy_set_header Host $http_host; + } + location /captcha/v2/media/ { proxy_pass http://libre-captcha:8888/v2/media/; proxy_set_header Host $http_host;