Skip to content
This repository was archived by the owner on Apr 4, 2026. It is now read-only.

Commit a940960

Browse files
refactor: simplify backfill queue to use only normal and inprogress q… (#43)
* refactor: simplify backfill queue to use only normal and inprogress queues - Remove high/low priority queues, keeping only normal and inprogress - Increase BRPOP timeout from 0.5s to 5s for better Docker compatibility - Update all logging and metrics to reflect simplified queue structure - Update inspect command to show only relevant queues - Priority field in BackfillJob is now ignored - Fixes early termination issue in Docker Compose environments * docs: update CHANGELOG with backfill queue changes
1 parent 840c30b commit a940960

4 files changed

Lines changed: 75 additions & 110 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to Waypoint will be documented in this file.
99
- Resolve backfill job queue exhaustion bug (#37)
1010
- Increase Redis pool sizes to prevent backfill queue exhaustion (#38)
1111
- Handle Redis XPENDING response variations and prevent backfill job loss (#39)
12+
- Fix backfill early termination in Docker Compose environments by increasing BRPOP timeout
1213

1314
### Documentation
1415

@@ -21,6 +22,10 @@ All notable changes to Waypoint will be documented in this file.
2122
- Custom headers (#42)
2223
- Update proto to 0.3.1
2324

25+
### Refactor
26+
27+
- Simplify backfill queue system to use only normal and inprogress queues (remove unused priority queues)
28+
2429
## [0.6.3] - 2025-06-09
2530

2631
### Bug Fixes

docker-compose.backfill.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ services:
117117
condition: service_healthy
118118
redis:
119119
condition: service_healthy
120-
backfill-queue:
121-
condition: service_started
122120
networks:
123121
- waypoint-network
124122

src/backfill/worker.rs

Lines changed: 68 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ impl Default for JobState {
6060
pub struct BackfillQueue {
6161
redis: Arc<Redis>,
6262
queue_key: String,
63-
high_priority_queue_key: String,
64-
low_priority_queue_key: String,
6563
in_progress_queue_key: String,
6664
metrics: Arc<tokio::sync::RwLock<QueueMetrics>>,
6765
}
@@ -73,9 +71,7 @@ pub struct QueueMetrics {
7371
pub jobs_failed: u64,
7472
pub fids_processed: u64,
7573
pub avg_job_time_ms: f64,
76-
pub high_priority_queue_size: u64,
7774
pub normal_priority_queue_size: u64,
78-
pub low_priority_queue_size: u64,
7975
pub in_progress_queue_size: u64,
8076
}
8177

@@ -90,8 +86,6 @@ impl BackfillQueue {
9086
Self {
9187
redis,
9288
queue_key: format!("{}:normal", base_key),
93-
high_priority_queue_key: format!("{}:high", base_key),
94-
low_priority_queue_key: format!("{}:low", base_key),
9589
in_progress_queue_key: format!("{}:inprogress", base_key),
9690
metrics: Arc::new(tokio::sync::RwLock::new(QueueMetrics::default())),
9791
}
@@ -101,9 +95,7 @@ impl BackfillQueue {
10195
/// Uses more efficient pattern without unnecessary cloning
10296
pub async fn get_metrics(&self) -> QueueMetrics {
10397
// First fetch the queue sizes without holding the lock
104-
let high = self.get_queue_length_for_key(&self.high_priority_queue_key).await.unwrap_or(0);
10598
let normal = self.get_queue_length_for_key(&self.queue_key).await.unwrap_or(0);
106-
let low = self.get_queue_length_for_key(&self.low_priority_queue_key).await.unwrap_or(0);
10799
let in_progress =
108100
self.get_queue_length_for_key(&self.in_progress_queue_key).await.unwrap_or(0);
109101

@@ -117,9 +109,7 @@ impl BackfillQueue {
117109
jobs_failed: metrics_guard.jobs_failed,
118110
fids_processed: metrics_guard.fids_processed,
119111
avg_job_time_ms: metrics_guard.avg_job_time_ms,
120-
high_priority_queue_size: high as u64,
121112
normal_priority_queue_size: normal as u64,
122-
low_priority_queue_size: low as u64,
123113
in_progress_queue_size: in_progress as u64,
124114
}
125115
}
@@ -156,28 +146,21 @@ impl BackfillQueue {
156146
job.id = uuid::Uuid::new_v4().to_string();
157147
}
158148

159-
// Select appropriate queue based on priority
160-
let queue_key = match job.priority {
161-
JobPriority::High => &self.high_priority_queue_key,
162-
JobPriority::Normal => &self.queue_key,
163-
JobPriority::Low => &self.low_priority_queue_key,
164-
};
149+
// Always use the normal queue now
150+
let queue_key = &self.queue_key;
165151

166152
// Serialize job once to avoid cloning
167153
let job_data = serde_json::to_string(&job)
168154
.map_err(|e| crate::redis::error::Error::DeserializationError(e.to_string()))?;
169155

170156
let fid_count = job.fids.len();
171157
let job_id = job.id.clone();
172-
// Use a reference to avoid copying non-Copy type
173-
let priority = &job.priority;
174158

175159
info!(
176-
"Adding backfill job {} with {} FIDs to queue: {} (priority: {:?}), FID range: {:?}..{:?}",
160+
"Adding backfill job {} with {} FIDs to queue: {}, FID range: {:?}..{:?}",
177161
job_id,
178162
fid_count,
179163
queue_key,
180-
priority,
181164
job.fids.first(),
182165
job.fids.last()
183166
);
@@ -239,67 +222,60 @@ impl BackfillQueue {
239222
.get_connection_with_timeout(2000) // 2 second timeout
240223
.await?;
241224

242-
// Try queues in priority order: high, normal, low
243-
let queue_keys =
244-
[&self.high_priority_queue_key, &self.queue_key, &self.low_priority_queue_key];
245-
246-
for queue_key in &queue_keys {
247-
// Use shorter timeout for BRPOP to avoid holding connections too long
248-
let result: RedisResult<Option<Vec<String>>> = bb8_redis::redis::cmd("BRPOP")
249-
.arg(queue_key)
250-
.arg(0.5) // Reduced timeout to 500ms
251-
.query_async(&mut *conn)
252-
.await;
253-
254-
match result {
255-
Ok(Some(values)) if values.len() >= 2 => {
256-
let job_data = &values[1];
257-
let mut job: BackfillJob = serde_json::from_str(job_data).map_err(|e| {
258-
crate::redis::error::Error::DeserializationError(e.to_string())
259-
})?;
260-
261-
job.state = JobState::InProgress;
262-
job.attempts += 1;
263-
job.started_at = Some(chrono::Utc::now()); // Set the start time
264-
265-
if job.visibility_timeout.is_none() {
266-
job.visibility_timeout = Some(300);
267-
}
225+
// Only check the normal queue now
226+
// Use longer timeout for BRPOP in containerized environments
227+
let result: RedisResult<Option<Vec<String>>> = bb8_redis::redis::cmd("BRPOP")
228+
.arg(&self.queue_key)
229+
.arg(5.0) // 5 second timeout for better Docker compatibility
230+
.query_async(&mut *conn)
231+
.await;
268232

269-
let in_progress_data = serde_json::to_string(&job).unwrap();
270-
let _: RedisResult<()> = bb8_redis::redis::cmd("LPUSH")
271-
.arg(&self.in_progress_queue_key)
272-
.arg(&in_progress_data)
273-
.query_async(&mut *conn)
274-
.await;
233+
match result {
234+
Ok(Some(values)) if values.len() >= 2 => {
235+
let job_data = &values[1];
236+
let mut job: BackfillJob = serde_json::from_str(job_data)
237+
.map_err(|e| crate::redis::error::Error::DeserializationError(e.to_string()))?;
275238

276-
// Don't use Redis expiration - we'll handle timeout in cleanup_expired_jobs
277-
// This prevents jobs from being lost when Redis keys expire
239+
job.state = JobState::InProgress;
240+
job.attempts += 1;
241+
job.started_at = Some(chrono::Utc::now()); // Set the start time
278242

279-
info!(
280-
"Retrieved backfill job {} with {} FIDs from queue {} (priority: {:?}, attempt: {}), FID range: {:?}..{:?}",
281-
job.id,
282-
job.fids.len(),
283-
queue_key,
284-
job.priority,
285-
job.attempts,
286-
job.fids.first(),
287-
job.fids.last()
288-
);
289-
debug!("FIDs in retrieved job: {:?}", job.fids);
290-
291-
return Ok(Some(job));
292-
},
293-
Ok(_) => continue,
294-
Err(e) => {
295-
error!("Error retrieving job from queue {}: {:?}", queue_key, e);
296-
return Err(crate::redis::error::Error::RedisError(e));
297-
},
298-
}
299-
}
243+
if job.visibility_timeout.is_none() {
244+
job.visibility_timeout = Some(300);
245+
}
246+
247+
let in_progress_data = serde_json::to_string(&job).unwrap();
248+
let _: RedisResult<()> = bb8_redis::redis::cmd("LPUSH")
249+
.arg(&self.in_progress_queue_key)
250+
.arg(&in_progress_data)
251+
.query_async(&mut *conn)
252+
.await;
253+
254+
// Don't use Redis expiration - we'll handle timeout in cleanup_expired_jobs
255+
// This prevents jobs from being lost when Redis keys expire
256+
257+
info!(
258+
"Retrieved backfill job {} with {} FIDs from queue {} (attempt: {}), FID range: {:?}..{:?}",
259+
job.id,
260+
job.fids.len(),
261+
&self.queue_key,
262+
job.attempts,
263+
job.fids.first(),
264+
job.fids.last()
265+
);
266+
debug!("FIDs in retrieved job: {:?}", job.fids);
300267

301-
debug!("No jobs found in any queue after checking all priority levels");
302-
Ok(None)
268+
Ok(Some(job))
269+
},
270+
Ok(_) => {
271+
debug!("No jobs found in queue");
272+
Ok(None)
273+
},
274+
Err(e) => {
275+
error!("Error retrieving job from queue {}: {:?}", &self.queue_key, e);
276+
Err(crate::redis::error::Error::RedisError(e))
277+
},
278+
}
303279
}
304280

305281
/// Mark a job as completed
@@ -360,11 +336,6 @@ impl BackfillQueue {
360336
job.state = JobState::Pending;
361337
// Don't increment attempts here - it was already incremented in get_job
362338

363-
// Reduce priority if job has been retried multiple times
364-
if job.attempts > 3 {
365-
job.priority = JobPriority::Low;
366-
}
367-
368339
// Add job back to queue (reusing the job object)
369340
self.add_job(job).await?;
370341

@@ -378,13 +349,9 @@ impl BackfillQueue {
378349
}
379350

380351
pub async fn get_queue_length(&self) -> Result<usize, crate::redis::error::Error> {
381-
let high = self.get_queue_length_for_key(&self.high_priority_queue_key).await?;
382352
let normal = self.get_queue_length_for_key(&self.queue_key).await?;
383-
let low = self.get_queue_length_for_key(&self.low_priority_queue_key).await?;
384-
385-
let total = high + normal + low;
386-
info!("Total queue length: {} (High: {}, Normal: {}, Low: {})", total, high, normal, low);
387-
Ok(total)
353+
info!("Total queue length: {}", normal);
354+
Ok(normal)
388355
}
389356

390357
/// Clean up expired jobs from the in-progress queue
@@ -649,17 +616,13 @@ impl Worker {
649616
if last_progress_log.elapsed() > Duration::from_secs(10) {
650617
let metrics = self.queue.get_metrics().await;
651618
info!(
652-
"Queue state - High: {}, Normal: {}, Low: {}, In Progress: {}, Total processed: {}",
653-
metrics.high_priority_queue_size,
619+
"Queue state - Normal: {}, In Progress: {}, Total processed: {}",
654620
metrics.normal_priority_queue_size,
655-
metrics.low_priority_queue_size,
656621
metrics.in_progress_queue_size,
657622
metrics.jobs_processed
658623
);
659624

660-
let total_queue_length = metrics.high_priority_queue_size
661-
+ metrics.normal_priority_queue_size
662-
+ metrics.low_priority_queue_size;
625+
let total_queue_length = metrics.normal_priority_queue_size;
663626
self.log_stats(total_queue_length as usize).await;
664627
last_progress_log = std::time::Instant::now();
665628
}
@@ -668,11 +631,8 @@ impl Worker {
668631
if active_tasks < self.concurrency {
669632
let pre_metrics = self.queue.get_metrics().await;
670633
debug!(
671-
"Attempting to get job. Current queue state - High: {}, Normal: {}, Low: {}, In Progress: {}",
672-
pre_metrics.high_priority_queue_size,
673-
pre_metrics.normal_priority_queue_size,
674-
pre_metrics.low_priority_queue_size,
675-
pre_metrics.in_progress_queue_size
634+
"Attempting to get job. Current queue state - Normal: {}, In Progress: {}",
635+
pre_metrics.normal_priority_queue_size, pre_metrics.in_progress_queue_size
676636
);
677637

678638
match self.queue.get_job().await {
@@ -869,9 +829,15 @@ impl Worker {
869829
},
870830
Ok(None) => {
871831
let post_metrics = self.queue.get_metrics().await;
872-
let total_queued = post_metrics.high_priority_queue_size
873-
+ post_metrics.normal_priority_queue_size
874-
+ post_metrics.low_priority_queue_size;
832+
let total_queued = post_metrics.normal_priority_queue_size;
833+
834+
// Log detailed state when no jobs found
835+
debug!(
836+
"No jobs retrieved. Queue state - Normal: {}, In Progress: {}, Active tasks: {}",
837+
post_metrics.normal_priority_queue_size,
838+
post_metrics.in_progress_queue_size,
839+
active_tasks
840+
);
875841

876842
if total_queued == 0 && post_metrics.in_progress_queue_size == 0 {
877843
info!("All queues are empty and no jobs in progress.");

src/commands/backfill/fid/inspect.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,10 @@ pub async fn execute(config: &Config, _args: &ArgMatches) -> Result<()> {
2828
info!(" Average job time: {:.2}ms", metrics.avg_job_time_ms);
2929
info!("");
3030
info!("Queue Sizes:");
31-
info!(" High priority queue: {} jobs", metrics.high_priority_queue_size);
32-
info!(" Normal priority queue: {} jobs", metrics.normal_priority_queue_size);
33-
info!(" Low priority queue: {} jobs", metrics.low_priority_queue_size);
31+
info!(" Normal queue: {} jobs", metrics.normal_priority_queue_size);
3432
info!(" In-progress queue: {} jobs", metrics.in_progress_queue_size);
3533

36-
let total_pending = metrics.high_priority_queue_size
37-
+ metrics.normal_priority_queue_size
38-
+ metrics.low_priority_queue_size;
34+
let total_pending = metrics.normal_priority_queue_size;
3935
info!("");
4036
info!("Total pending jobs: {}", total_pending);
4137

0 commit comments

Comments
 (0)