Skip to content

Commit c8bd01b

Browse files
Align sleep timing tests with SDK parity
1 parent 92a5192 commit c8bd01b

2 files changed

Lines changed: 114 additions & 26 deletions

File tree

src/context.rs

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::task_result::{
44
fetch_task_result_snapshot,
55
};
66
use crate::types::{ClaimedTask, Json, duration_seconds_ceil, validate_queue_name};
7-
use chrono::{DateTime, Utc};
7+
use chrono::{DateTime, Timelike, Utc};
88
use deadpool_postgres::Pool;
99
use serde::{Serialize, de::DeserializeOwned};
1010
use serde_json::{Map, Value};
@@ -204,22 +204,32 @@ impl TaskContext {
204204
}
205205

206206
pub async fn sleep_for(&mut self, name: impl AsRef<str>, duration: Duration) -> Result<()> {
207-
self.sleep_until(
208-
name,
209-
Utc::now()
210-
+ chrono::Duration::from_std(duration).map_err(|_| {
211-
Error::Config("sleep duration is too large for chrono".to_string())
212-
})?,
213-
)
214-
.await
207+
let duration = chrono::Duration::from_std(duration)
208+
.map_err(|_| Error::Config("sleep duration is too large for chrono".to_string()))?;
209+
let name = name.as_ref().to_string();
210+
let now = self.current_time().await?;
211+
self.sleep_until_at_current_time(&name, now + duration, now)
212+
.await
215213
}
216214

217215
pub async fn sleep_until(
218216
&mut self,
219217
name: impl AsRef<str>,
220218
wake_at: DateTime<Utc>,
221219
) -> Result<()> {
222-
let checkpoint_name = self.next_checkpoint_name(name.as_ref());
220+
let name = name.as_ref().to_string();
221+
let now = self.current_time().await?;
222+
self.sleep_until_at_current_time(&name, wake_at, now).await
223+
}
224+
225+
async fn sleep_until_at_current_time(
226+
&mut self,
227+
name: &str,
228+
wake_at: DateTime<Utc>,
229+
now: DateTime<Utc>,
230+
) -> Result<()> {
231+
let checkpoint_name = self.next_checkpoint_name(name);
232+
let wake_at = postgres_timestamp_precision(wake_at);
223233
let actual_wake_at = if let Some(cached) = self.lookup_checkpoint(&checkpoint_name).await? {
224234
serde_json::from_value(cached)?
225235
} else {
@@ -228,7 +238,7 @@ impl TaskContext {
228238
wake_at
229239
};
230240

231-
if Utc::now() < actual_wake_at {
241+
if now < actual_wake_at {
232242
self.schedule_run(actual_wake_at).await?;
233243
return Err(Error::Suspended);
234244
}
@@ -490,13 +500,29 @@ impl TaskContext {
490500
Ok(())
491501
}
492502

503+
async fn current_time(&self) -> Result<DateTime<Utc>> {
504+
let client = self.pool.get().await?;
505+
let row = client
506+
.query_one("SELECT absurd.current_time()", &[])
507+
.await
508+
.map_err(map_database_error)?;
509+
Ok(row.get(0))
510+
}
511+
493512
fn notify_lease_extended(&self, duration: Duration) {
494513
if let Some(tx) = &self.lease_tx {
495514
let _ = tx.send(duration);
496515
}
497516
}
498517
}
499518

519+
fn postgres_timestamp_precision(timestamp: DateTime<Utc>) -> DateTime<Utc> {
520+
let truncated_nanos = timestamp.timestamp_subsec_micros() * 1_000;
521+
timestamp
522+
.with_nanosecond(truncated_nanos)
523+
.unwrap_or(timestamp)
524+
}
525+
500526
async fn extend_claim(
501527
pool: &Pool,
502528
queue_name: &str,
@@ -515,6 +541,34 @@ async fn extend_claim(
515541
Ok(())
516542
}
517543

544+
#[cfg(test)]
545+
mod tests {
546+
use super::*;
547+
548+
#[test]
549+
fn postgres_timestamp_precision_truncates_sub_microsecond_nanos() -> Result<()> {
550+
let timestamp = DateTime::from_timestamp(1_777_000_000, 123_456_789)
551+
.ok_or_else(|| Error::Config("valid timestamp".to_string()))?;
552+
553+
let normalized = postgres_timestamp_precision(timestamp);
554+
555+
assert_eq!(normalized.timestamp(), timestamp.timestamp());
556+
assert_eq!(normalized.timestamp_subsec_nanos(), 123_456_000);
557+
Ok(())
558+
}
559+
560+
#[test]
561+
fn postgres_timestamp_precision_preserves_microsecond_values() -> Result<()> {
562+
let timestamp = DateTime::from_timestamp(1_777_000_000, 123_456_000)
563+
.ok_or_else(|| Error::Config("valid timestamp".to_string()))?;
564+
565+
let normalized = postgres_timestamp_precision(timestamp);
566+
567+
assert_eq!(normalized, timestamp);
568+
Ok(())
569+
}
570+
}
571+
518572
#[derive(Clone, Debug, Default)]
519573
#[non_exhaustive]
520574
pub struct AwaitEventOptions {

tests/integration.rs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,44 +1486,78 @@ async fn repeated_step_names_are_numbered() -> Result<()> {
14861486

14871487
#[tokio::test]
14881488
#[ignore = "requires a Postgres database initialized with Absurd SQL"]
1489-
async fn sleep_until_suspends_then_resumes_from_checkpoint() -> Result<()> {
1490-
let (queue, client) = test_client().await?;
1489+
async fn sleep_for_suspends_until_duration_elapses() -> Result<()> {
1490+
let (queue, client, pool) = test_client_with_single_connection_pool().await?;
1491+
let base = utc("2024-05-05T10:00:00Z")?;
1492+
set_fake_now(&pool, Some(base)).await?;
1493+
1494+
let task = Task::<(), Value>::new("sleep-for").queue(&queue);
1495+
client.register(&task, |(), mut ctx| async move {
1496+
ctx.sleep_for("wait-for", Duration::from_secs(60)).await?;
1497+
Ok(json!({ "resumed": true }))
1498+
})?;
1499+
1500+
let spawned = client.spawn(&task, (), Default::default()).await?;
1501+
assert_eq!(client.work_batch(WorkBatchOptions::new()).await?, 1);
1502+
1503+
let task = fetch_task(&queue, spawned.task_id).await?;
1504+
let run = fetch_run(&queue, spawned.run_id).await?;
1505+
let wake_at = base + ChronoDuration::seconds(60);
1506+
assert_eq!(task.state, "sleeping");
1507+
assert_eq!(run.state, "sleeping");
1508+
assert_eq!(run.available_at, Some(wake_at));
1509+
1510+
set_fake_now(&pool, Some(wake_at + ChronoDuration::seconds(5))).await?;
1511+
assert_eq!(client.work_batch(WorkBatchOptions::new()).await?, 1);
1512+
1513+
let task = fetch_task(&queue, spawned.task_id).await?;
1514+
assert_eq!(task.state, "completed");
1515+
assert_eq!(task.completed_payload, Some(json!({ "resumed": true })));
1516+
1517+
client.drop_queue().await?;
1518+
Ok(())
1519+
}
1520+
1521+
#[tokio::test]
1522+
#[ignore = "requires a Postgres database initialized with Absurd SQL"]
1523+
async fn sleep_until_checkpoint_prevents_rescheduling() -> Result<()> {
1524+
let (queue, client, pool) = test_client_with_single_connection_pool().await?;
1525+
let base = utc("2024-05-06T09:00:00Z")?;
1526+
set_fake_now(&pool, Some(base)).await?;
1527+
let wake_at = base + ChronoDuration::minutes(5);
14911528
let executions = Arc::new(AtomicUsize::new(0));
14921529

1493-
let task = Task::<(), Value>::new("sleepy").queue(&queue);
1530+
let task = Task::<(), Value>::new("sleep-until").queue(&queue);
14941531
client.register(&task, {
14951532
let executions = Arc::clone(&executions);
14961533
move |(), mut ctx| {
14971534
let executions = Arc::clone(&executions);
14981535
async move {
14991536
let execution = executions.fetch_add(1, Ordering::SeqCst) + 1;
1500-
let wake_at = Utc::now() + ChronoDuration::seconds(1);
1501-
ctx.sleep_until("pause", wake_at).await?;
1537+
ctx.sleep_until("sleep-step", wake_at).await?;
15021538
Ok(json!({ "executions": execution }))
15031539
}
15041540
}
15051541
})?;
15061542

15071543
let spawned = client.spawn(&task, (), Default::default()).await?;
1508-
15091544
assert_eq!(client.work_batch(WorkBatchOptions::new()).await?, 1);
15101545

1546+
let checkpoints = fetch_checkpoints(&queue, spawned.task_id).await?;
1547+
assert_eq!(
1548+
checkpoints,
1549+
vec![("sleep-step".to_string(), serde_json::to_value(wake_at)?)],
1550+
);
1551+
15111552
let task = fetch_task(&queue, spawned.task_id).await?;
15121553
let run = fetch_run(&queue, spawned.run_id).await?;
15131554
assert_eq!(task.state, "sleeping");
15141555
assert_eq!(run.state, "sleeping");
15151556
assert!(run.wake_event.is_none());
15161557
assert!(run.failure_reason.is_none());
1558+
assert_eq!(run.available_at, Some(wake_at));
15171559

1518-
let checkpoints = fetch_checkpoints(&queue, spawned.task_id).await?;
1519-
assert_eq!(checkpoints.len(), 1);
1520-
assert_eq!(checkpoints[0].0, "pause");
1521-
let checkpoint_wake: DateTime<Utc> = serde_json::from_value(checkpoints[0].1.clone())?;
1522-
assert_eq!(run.available_at, Some(checkpoint_wake));
1523-
1524-
assert_eq!(client.work_batch(WorkBatchOptions::new()).await?, 0);
1525-
1526-
tokio::time::sleep(Duration::from_millis(1_100)).await;
1560+
set_fake_now(&pool, Some(wake_at)).await?;
15271561
assert_eq!(client.work_batch(WorkBatchOptions::new()).await?, 1);
15281562

15291563
let task = fetch_task(&queue, spawned.task_id).await?;

0 commit comments

Comments
 (0)