Skip to content

Commit 4bcec6e

Browse files
committed
Reserve transportation reclassification before enqueue
1 parent 501c12c commit 4bcec6e

4 files changed

Lines changed: 110 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88

99
### Fixed
1010

11+
- Reserve transportation reclassification while its worker is queued, preventing duplicate runs from repeated requests and allowing retry after enqueue failure.
12+
1113
- Prometheus metrics from web and Sidekiq no longer produce duplicate series when label order differs (#3769).
1214
- KMZ imports no longer repeatedly re-import their own archive and fill the import queue and storage (#3766).
1315

‎app/controllers/tracks/recalculations_controller.rb‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ class Tracks::RecalculationsController < ApplicationController
88
def create
99
status = Tracks::TransportationRecalculationStatus.new(current_user.id)
1010

11-
if status.in_progress?
11+
enqueued = status.enqueue { TransportationModes::UserReclassifyJob.perform_later(current_user.id) }
12+
13+
if !enqueued
1214
respond_to do |format|
1315
format.turbo_stream do
1416
render turbo_stream: stream_flash(
@@ -22,7 +24,6 @@ def create
2224
end
2325
end
2426
else
25-
TransportationModes::UserReclassifyJob.perform_later(current_user.id)
2627
respond_to do |format|
2728
format.turbo_stream do
2829
render turbo_stream: stream_flash(

‎app/services/tracks/transportation_recalculation_status.rb‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ def data
2727
Rails.cache.read(cache_key) || { 'status' => 'idle' }
2828
end
2929

30+
def enqueue
31+
User.with_advisory_lock("transportation_recalculation:#{user_id}") do
32+
return false if in_progress?
33+
34+
begin
35+
raise IOError, 'Reclassification status could not be reserved' unless start(total_tracks: 0)
36+
37+
job = yield
38+
raise ActiveJob::EnqueueError, 'Reclassification could not be queued' unless job.try(:successfully_enqueued?)
39+
rescue StandardError => e
40+
fail(e.message)
41+
raise
42+
end
43+
true
44+
end
45+
end
46+
3047
def start(total_tracks:)
3148
reset_counter
3249
Rails.cache.write(
@@ -49,10 +66,12 @@ def increment_processed!
4966
# hash would resurrect the short-lived 'completed' entry with CACHE_TTL.
5067
return data['processed_tracks'].to_i if current_status == 'completed'
5168

52-
count = Rails.cache.increment(counter_key, 1, expires_in: CACHE_TTL) || 1
5369
total = data['total_tracks']
70+
return data['processed_tracks'].to_i if total&.zero?
71+
72+
count = Rails.cache.increment(counter_key, 1, expires_in: CACHE_TTL) || 1
5473
update_progress(processed_tracks: count, total_tracks: total)
55-
complete if total && count >= total
74+
complete if total&.positive? && count >= total
5675
count
5776
end
5877

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Queued transportation reclassification', type: :request do
6+
let(:user) { create(:user) }
7+
let(:progress) { Tracks::TransportationRecalculationStatus.new(user.id) }
8+
9+
before do
10+
sign_in user
11+
clear_enqueued_jobs
12+
end
13+
14+
def request_reclassification
15+
post tracks_recalculation_path, headers: { 'Accept' => 'text/vnd.turbo-stream.html' }
16+
end
17+
18+
it 'enqueues once while the first fan-out is still waiting in the queue' do
19+
request_reclassification
20+
request_reclassification
21+
22+
expect(TransportationModes::UserReclassifyJob).to have_been_enqueued.exactly(:once).with(user.id)
23+
expect(progress.in_progress?).to be true
24+
expect(progress.data).to include('status' => 'processing', 'total_tracks' => 0, 'processed_tracks' => 0)
25+
expect(response.body).to include('already running')
26+
end
27+
28+
it 'keeps a waiting reservation when an older per-track job reports late progress' do
29+
request_reclassification
30+
progress.increment_processed!
31+
request_reclassification
32+
33+
expect(progress.current_status).to eq('processing')
34+
expect(TransportationModes::UserReclassifyJob).to have_been_enqueued.exactly(:once).with(user.id)
35+
end
36+
37+
it 'does not write late track progress into a waiting reservation' do
38+
request_reclassification
39+
before_progress = progress.data
40+
expect(progress).not_to receive(:update_progress)
41+
42+
expect(progress.increment_processed!).to eq(0)
43+
expect(progress.data).to eq(before_progress)
44+
end
45+
46+
it 'does not enqueue work when its reservation cannot be stored' do
47+
allow(Rails.cache).to receive(:write).and_call_original
48+
allow(Rails.cache).to receive(:write).with(progress.cache_key, anything, expires_in: anything).and_return(false)
49+
50+
expect { request_reclassification }.to raise_error(IOError)
51+
expect(TransportationModes::UserReclassifyJob).not_to have_been_enqueued
52+
end
53+
54+
it 'lets a completed run be requested again' do
55+
request_reclassification
56+
progress.start(total_tracks: 1)
57+
progress.complete
58+
request_reclassification
59+
60+
expect(TransportationModes::UserReclassifyJob).to have_been_enqueued.exactly(:twice).with(user.id)
61+
end
62+
63+
it 'releases a reservation after the enqueue adapter rejects the job' do
64+
adapter = ActiveJob::Base.queue_adapter
65+
allow(adapter).to receive(:enqueue).and_raise(ActiveJob::EnqueueError, 'synthetic enqueue failure')
66+
67+
expect { request_reclassification }.to raise_error(ActiveJob::EnqueueError)
68+
expect(progress.in_progress?).to be false
69+
allow(adapter).to receive(:enqueue).and_call_original
70+
sign_in user
71+
request_reclassification
72+
73+
expect(TransportationModes::UserReclassifyJob).to have_been_enqueued.exactly(:once).with(user.id)
74+
end
75+
76+
it 'turns the queued reservation into ordinary progress when the worker starts' do
77+
request_reclassification
78+
TransportationModes::UserReclassifyJob.perform_now(user.id)
79+
80+
expect(progress.current_status).to eq('completed')
81+
request_reclassification
82+
expect(TransportationModes::UserReclassifyJob).to have_been_enqueued.exactly(:twice).with(user.id)
83+
end
84+
end

0 commit comments

Comments
 (0)