Skip to content

Conversation

@davidalejandroaguilar
Copy link

Description

Summary

They were failing because the timestamp argument passed to ActiveJob::QueueAdapters::AsyncJobAdapter#enqueue_at is a float, not a ActiveSupport::TimeWithZone / Time object.

The adapter was setting job.scheduled_at = timestamp, which caused ActiveJob::Core#serialize to fail with undefined method 'utc' for an instance of Float because it does: "scheduled_at" => scheduled_at ? scheduled_at.utc.iso8601(9) : nil.

Fix

This PR fixes the above by using ||= to set the job.scheduled_at to a Time object via Time.at.

This is not really necessary since when jobs are created via set, they already come in with scheduled_at set as a ActiveSupport::TimeWithZone object.

Since this is an Active Job adapter, I'd suggest just removing it since we know jobs will already come with scheduled_at set, but I'm leaving it there just in case we want to guarantee that #enqueue_at sets it on the job when passing a timestamp.

Detailed stack trace

  1. lib/active_job/enqueuing.rb#_raw_enqueue calls queue_adapter.enqueue_at self, scheduled_at.to_f (notice it converts the timestamp to a float)
    source

  2. lib/active_job/queue_adapters/async_job_adapter.rb#enqueue_at receives timestamp, e.g. 1761485982.6905391 source.

  3. lib/active_job/queue_adapters/async_job_adapter.rb#enqueue_at sets it on job.scheduled_at = timestamp
    source

  4. lib/active_job/queue_adapters/async_job_adapter.rb#enqueue_at calls @dispatcher.call(job) source.

  5. lib/async/job/adapter/active_job/dispatcher.rb#call calls job.serialize source

  6. lib/active_job/core.rb#serialize calls "scheduled_at" => scheduled_at ? scheduled_at.utc.iso8601(9) : nil source

  7. undefined method 'utc' for an instance of Float because scheduled_at was set to the timestamp.

Types of Changes

  • Bug fix.

Contribution

* They were failing because `timestamp` is a float,
not a `ActiveSupport::TimeWithZone` object.
* When jobs are created via `set`, they already come
in with `scheduled_at` set as a `ActiveSupport::TimeWithZone` object.
* However, just in case they don't, and expect #enqueue_at
to work when passing a timestamp, we convert it to a
`ActiveSupport::TimeWithZone` object and set it on
`job.scheduled_at`.

1. `lib/active_job/enqueuing.rb#_raw_enqueue` calls
`queue_adapter.enqueue_at self, scheduled_at.to_f`
(notice it converts the timestamp to a float)
[source](https://github.com/rails/rails/blob/v8.1.0/activejob/lib/active_job/enqueuing.rb#L134)

2. `lib/active_job/queue_adapters/async_job_adapter.rb#enqueue_at`
receives `timestamp`, e.g. `1761485982.6905391`.

3. `lib/active_job/queue_adapters/async_job_adapter.rb#enqueue_at`
sets it on `job.scheduled_at = timestamp`
[source](https://github.com/socketry/async-job-adapter-active_job/blob/v0.18.3/lib/active_job/queue_adapters/async_job_adapter.rb#L34)

4. `lib/active_job/queue_adapters/async_job_adapter.rb#enqueue_at`
calls `@dispatcher.call(job)`

5. `lib/async/job/adapter/active_job/dispatcher.rb#call` calls
`job.serialize` [source](https://github.com/socketry/async-job-adapter-active_job/blob/v0.18.3/lib/async/job/adapter/active_job/dispatcher.rb#L50)

6. `lib/active_job/core.rb#serialize` calls
`"scheduled_at" => scheduled_at ? scheduled_at.utc.iso8601(9) : nil`
[source](https://github.com/rails/rails/blob/v8.1.0/activejob/lib/active_job/core.rb#L130)

7. `undefined method 'utc' for an instance of Float` because
`scheduled_at` was set to the timestamp.
before do
dispatcher.queues["default"] = queue
job.set(wait:)
end
Copy link
Author

@davidalejandroaguilar davidalejandroaguilar Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other spec tests that calling adapter.enqueue_at(job, timestamp.to_f) without explicitly setting scheduled_at on the job via set will successfully set scheduled_at.

This new spec tests that setting it on the job as you're supposed to (via set) will work.

We might not want to support the former test, but again, leaving it just in case we want to guarantee that.

# @parameter timestamp [Time] The time at which to enqueue the job.
def enqueue_at(job, timestamp)
job.scheduled_at = timestamp
job.scheduled_at ||= Time.at(timestamp)
Copy link
Author

@davidalejandroaguilar davidalejandroaguilar Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, since this is an Active Job adapter, I'd suggest just removing it since we know jobs will already come with scheduled_at set, but I'm leaving it there just in case we want to guarantee that #enqueue_at sets it on the job when passing a timestamp.

I understand that commit 3b4fd0b - Fix handling of scheduled jobs. added this so there might be a more nuanced reason why we make sure its set here, so that's yet another reason why I'm not blindly removing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant